Home | History | Annotate | Download | only in js
      1 var classesNav;
      2 var devdocNav;
      3 var sidenav;
      4 var cookie_namespace = 'android_developer';
      5 var NAV_PREF_TREE = "tree";
      6 var NAV_PREF_PANELS = "panels";
      7 var nav_pref;
      8 var isMobile = false; // true if mobile, so we can adjust some layout
      9 var mPagePath; // initialized in ready() function
     10 
     11 var basePath = getBaseUri(location.pathname);
     12 var SITE_ROOT = toRoot + basePath.substring(1,basePath.indexOf("/",1));
     13 var GOOGLE_DATA; // combined data for google service apis, used for search suggest
     14 
     15 // Ensure that all ajax getScript() requests allow caching
     16 $.ajaxSetup({
     17   cache: true
     18 });
     19 
     20 /******  ON LOAD SET UP STUFF *********/
     21 
     22 $(document).ready(function() {
     23 
     24   // show lang dialog if the URL includes /intl/
     25   //if (location.pathname.substring(0,6) == "/intl/") {
     26   //  var lang = location.pathname.split('/')[2];
     27    // if (lang != getLangPref()) {
     28    //   $("#langMessage a.yes").attr("onclick","changeLangPref('" + lang
     29    //       + "', true); $('#langMessage').hide(); return false;");
     30   //    $("#langMessage .lang." + lang).show();
     31    //   $("#langMessage").show();
     32    // }
     33   //}
     34 
     35   // load json file for JD doc search suggestions
     36   $.getScript(toRoot + 'jd_lists_unified.js');
     37   // load json file for Android API search suggestions
     38   $.getScript(toRoot + 'reference/lists.js');
     39   // load json files for Google services API suggestions
     40   $.getScript(toRoot + 'reference/gcm_lists.js', function(data, textStatus, jqxhr) {
     41       // once the GCM json (GCM_DATA) is loaded, load the GMS json (GMS_DATA) and merge the data
     42       if(jqxhr.status === 200) {
     43           $.getScript(toRoot + 'reference/gms_lists.js', function(data, textStatus, jqxhr) {
     44               if(jqxhr.status === 200) {
     45                   // combine GCM and GMS data
     46                   GOOGLE_DATA = GMS_DATA;
     47                   var start = GOOGLE_DATA.length;
     48                   for (var i=0; i<GCM_DATA.length; i++) {
     49                       GOOGLE_DATA.push({id:start+i, label:GCM_DATA[i].label,
     50                               link:GCM_DATA[i].link, type:GCM_DATA[i].type});
     51                   }
     52               }
     53           });
     54       }
     55   });
     56 
     57   // setup keyboard listener for search shortcut
     58   $('body').keyup(function(event) {
     59     if (event.which == 191 && $(event.target).is(':not(:input)')) {
     60       $('#search_autocomplete').focus();
     61     }
     62   });
     63 
     64   // init the fullscreen toggle click event
     65   $('#nav-swap .fullscreen').click(function(){
     66     if ($(this).hasClass('disabled')) {
     67       toggleFullscreen(true);
     68     } else {
     69       toggleFullscreen(false);
     70     }
     71   });
     72 
     73   // initialize the divs with custom scrollbars
     74   if (window.innerWidth >= 720) {
     75     $('.scroll-pane').jScrollPane({verticalGutter: 0});
     76   }
     77 
     78   // set up the search close button
     79   $('#search-close').on('click touchend', function() {
     80     $searchInput = $('#search_autocomplete');
     81     $searchInput.attr('value', '');
     82     $(this).addClass("hide");
     83     $("#search-container").removeClass('active');
     84     $("#search_autocomplete").blur();
     85     search_focus_changed($searchInput.get(), false);
     86     hideResults();
     87   });
     88 
     89 
     90   //Set up search
     91   $("#search_autocomplete").focus(function() {
     92     $("#search-container").addClass('active');
     93   })
     94   $("#search-container").on('mouseover touchend', function(e) {
     95     if ($(e.target).is('#search-close')) { return; }
     96     $("#search-container").addClass('active');
     97     $("#search_autocomplete").focus();
     98   })
     99   $("#search-container").mouseout(function() {
    100     if ($("#search_autocomplete").is(":focus")) return;
    101     if ($("#search_autocomplete").val() == '') {
    102       setTimeout(function(){
    103         $("#search-container").removeClass('active');
    104         $("#search_autocomplete").blur();
    105       },250);
    106     }
    107   })
    108   $("#search_autocomplete").blur(function() {
    109     if ($("#search_autocomplete").val() == '') {
    110       $("#search-container").removeClass('active');
    111     }
    112   })
    113 
    114 
    115   // prep nav expandos
    116   var pagePath = document.location.pathname;
    117   // account for intl docs by removing the intl/*/ path
    118   if (pagePath.indexOf("/intl/") == 0) {
    119     pagePath = pagePath.substr(pagePath.indexOf("/",6)); // start after intl/ to get last /
    120   }
    121 
    122   if (pagePath.indexOf(SITE_ROOT) == 0) {
    123     if (pagePath == '' || pagePath.charAt(pagePath.length - 1) == '/') {
    124       pagePath += 'index.html';
    125     }
    126   }
    127 
    128   // Need a copy of the pagePath before it gets changed in the next block;
    129   // it's needed to perform proper tab highlighting in offline docs (see rootDir below)
    130   var pagePathOriginal = pagePath;
    131   if (SITE_ROOT.match(/\.\.\//) || SITE_ROOT == '') {
    132     // If running locally, SITE_ROOT will be a relative path, so account for that by
    133     // finding the relative URL to this page. This will allow us to find links on the page
    134     // leading back to this page.
    135     var pathParts = pagePath.split('/');
    136     var relativePagePathParts = [];
    137     var upDirs = (SITE_ROOT.match(/(\.\.\/)+/) || [''])[0].length / 3;
    138     for (var i = 0; i < upDirs; i++) {
    139       relativePagePathParts.push('..');
    140     }
    141     for (var i = 0; i < upDirs; i++) {
    142       relativePagePathParts.push(pathParts[pathParts.length - (upDirs - i) - 1]);
    143     }
    144     relativePagePathParts.push(pathParts[pathParts.length - 1]);
    145     pagePath = relativePagePathParts.join('/');
    146   } else {
    147     // Otherwise the page path is already an absolute URL
    148   }
    149 
    150   // Highlight the header tabs...
    151   // highlight Design tab
    152   var urlSegments = pagePathOriginal.split('/');
    153   var navEl = $(".dac-nav-list");
    154   var subNavEl = navEl.find(".dac-nav-secondary");
    155   var parentNavEl;
    156 
    157   if ($("body").hasClass("design")) {
    158     navEl.find("> li.design > a").addClass("selected");
    159   // highlight About tabs
    160   } else if ($("body").hasClass("about")) {
    161     if (urlSegments[1] == "about" || urlSegments[1] == "wear" || urlSegments[1] == "tv" || urlSegments[1] == "auto") {
    162       navEl.find("> li.home > a").addClass('has-subnav');
    163       subNavEl.find("li." + urlSegments[1] + " > a").addClass("selected");
    164     } else {
    165       navEl.find("> li.home > a").addClass('selected');
    166     }
    167 
    168 // highlight NDK tabs
    169   } else if ($("body").hasClass("ndk")) {
    170     parentNavEl = navEl.find("> li.ndk > a");
    171     parentNavEl.addClass('has-subnav');
    172     if ($("body").hasClass("guide")) {
    173       navEl.find("> li.guides > a").addClass("selected ndk");
    174     } else if ($("body").hasClass("reference")) {
    175       navEl.find("> li.reference > a").addClass("selected ndk");
    176     } else if ($("body").hasClass("samples")) {
    177       navEl.find("> li.samples > a").addClass("selected ndk");
    178     } else if ($("body").hasClass("downloads")) {
    179       navEl.find("> li.downloads > a").addClass("selected ndk");
    180     }
    181 
    182   // highlight Develop tab
    183   } else if ($("body").hasClass("develop") || $("body").hasClass("google")) {
    184     parentNavEl = navEl.find("> li.develop > a");
    185     parentNavEl.addClass('has-subnav');
    186 
    187     // In Develop docs, also highlight appropriate sub-tab
    188     if (urlSegments[1] == "training") {
    189       subNavEl.find("li.training > a").addClass("selected");
    190     } else if (urlSegments[1] == "guide") {
    191       subNavEl.find("li.guide > a").addClass("selected");
    192     } else if (urlSegments[1] == "reference") {
    193       // If the root is reference, but page is also part of Google Services, select Google
    194       if ($("body").hasClass("google")) {
    195         subNavEl.find("li.google > a").addClass("selected");
    196       } else {
    197         subNavEl.find("li.reference > a").addClass("selected");
    198       }
    199     } else if ((urlSegments[1] == "tools") || (urlSegments[1] == "sdk")) {
    200       subNavEl.find("li.tools > a").addClass("selected");
    201     } else if ($("body").hasClass("google")) {
    202       subNavEl.find("li.google > a").addClass("selected");
    203     } else if ($("body").hasClass("samples")) {
    204       subNavEl.find("li.samples > a").addClass("selected");
    205     } else if ($("body").hasClass("preview")) {
    206       subNavEl.find("li.preview > a").addClass("selected");
    207     } else {
    208       parentNavEl.removeClass('has-subnav').addClass("selected");
    209     }
    210   // highlight Distribute tab
    211   } else if ($("body").hasClass("distribute")) {
    212     parentNavEl = navEl.find("> li.distribute > a");
    213     parentNavEl.addClass('has-subnav');
    214 
    215     if (urlSegments[2] == "users") {
    216       subNavEl.find("li.users > a").addClass("selected");
    217     } else if (urlSegments[2] == "engage") {
    218       subNavEl.find("li.engage > a").addClass("selected");
    219     } else if (urlSegments[2] == "monetize") {
    220       subNavEl.find("li.monetize > a").addClass("selected");
    221     } else if (urlSegments[2] == "analyze") {
    222       subNavEl.find("li.analyze > a").addClass("selected");
    223     } else if (urlSegments[2] == "tools") {
    224       subNavEl.find("li.essentials > a").addClass("selected");
    225     } else if (urlSegments[2] == "stories") {
    226       subNavEl.find("li.stories > a").addClass("selected");
    227     } else if (urlSegments[2] == "essentials") {
    228       subNavEl.find("li.essentials > a").addClass("selected");
    229     } else if (urlSegments[2] == "googleplay") {
    230       subNavEl.find("li.googleplay > a").addClass("selected");
    231     } else {
    232       parentNavEl.removeClass('has-subnav').addClass("selected");
    233     }
    234   }
    235 
    236   // set global variable so we can highlight the sidenav a bit later (such as for google reference)
    237   // and highlight the sidenav
    238   mPagePath = pagePath;
    239   highlightSidenav();
    240   buildBreadcrumbs();
    241 
    242   // set up prev/next links if they exist
    243   var $selNavLink = $('#nav').find('a[href="' + pagePath + '"]');
    244   var $selListItem;
    245   if ($selNavLink.length) {
    246     $selListItem = $selNavLink.closest('li');
    247 
    248     // set up prev links
    249     var $prevLink = [];
    250     var $prevListItem = $selListItem.prev('li');
    251 
    252     var crossBoundaries = ($("body.design").length > 0) || ($("body.guide").length > 0) ? true :
    253 false; // navigate across topic boundaries only in design docs
    254     if ($prevListItem.length) {
    255       if ($prevListItem.hasClass('nav-section') || crossBoundaries) {
    256         // jump to last topic of previous section
    257         $prevLink = $prevListItem.find('a:last');
    258       } else if (!$selListItem.hasClass('nav-section')) {
    259         // jump to previous topic in this section
    260         $prevLink = $prevListItem.find('a:eq(0)');
    261       }
    262     } else {
    263       // jump to this section's index page (if it exists)
    264       var $parentListItem = $selListItem.parents('li');
    265       $prevLink = $selListItem.parents('li').find('a');
    266 
    267       // except if cross boundaries aren't allowed, and we're at the top of a section already
    268       // (and there's another parent)
    269       if (!crossBoundaries && $parentListItem.hasClass('nav-section')
    270                            && $selListItem.hasClass('nav-section')) {
    271         $prevLink = [];
    272       }
    273     }
    274 
    275     // set up next links
    276     var $nextLink = [];
    277     var startClass = false;
    278     var isCrossingBoundary = false;
    279 
    280     if ($selListItem.hasClass('nav-section') && $selListItem.children('div.empty').length == 0) {
    281       // we're on an index page, jump to the first topic
    282       $nextLink = $selListItem.find('ul:eq(0)').find('a:eq(0)');
    283 
    284       // if there aren't any children, go to the next section (required for About pages)
    285       if($nextLink.length == 0) {
    286         $nextLink = $selListItem.next('li').find('a');
    287       } else if ($('.topic-start-link').length) {
    288         // as long as there's a child link and there is a "topic start link" (we're on a landing)
    289         // then set the landing page "start link" text to be the first doc title
    290         $('.topic-start-link').text($nextLink.text().toUpperCase());
    291       }
    292 
    293       // If the selected page has a description, then it's a class or article homepage
    294       if ($selListItem.find('a[description]').length) {
    295         // this means we're on a class landing page
    296         startClass = true;
    297       }
    298     } else {
    299       // jump to the next topic in this section (if it exists)
    300       $nextLink = $selListItem.next('li').find('a:eq(0)');
    301       if ($nextLink.length == 0) {
    302         isCrossingBoundary = true;
    303         // no more topics in this section, jump to the first topic in the next section
    304         $nextLink = $selListItem.parents('li:eq(0)').next('li').find('a:eq(0)');
    305         if (!$nextLink.length) {  // Go up another layer to look for next page (lesson > class > course)
    306           $nextLink = $selListItem.parents('li:eq(1)').next('li.nav-section').find('a:eq(0)');
    307           if ($nextLink.length == 0) {
    308             // if that doesn't work, we're at the end of the list, so disable NEXT link
    309             $('.next-page-link').attr('href','').addClass("disabled")
    310                                 .click(function() { return false; });
    311             // and completely hide the one in the footer
    312             $('.content-footer .next-page-link').hide();
    313           }
    314         }
    315       }
    316     }
    317 
    318     if (startClass) {
    319       $('.start-class-link').attr('href', $nextLink.attr('href')).removeClass("hide");
    320 
    321       // if there's no training bar (below the start button),
    322       // then we need to add a bottom border to button
    323       if (!$("#tb").length) {
    324         $('.start-class-link').css({'border-bottom':'1px solid #DADADA'});
    325       }
    326     } else if (isCrossingBoundary && !$('body.design').length) {  // Design always crosses boundaries
    327       $('.content-footer.next-class').show();
    328       $('.next-page-link').attr('href','')
    329                           .removeClass("hide").addClass("disabled")
    330                           .click(function() { return false; });
    331       // and completely hide the one in the footer
    332       $('.content-footer .next-page-link').hide();
    333       if ($nextLink.length) {
    334         $('.next-class-link').attr('href',$nextLink.attr('href'))
    335                              .removeClass("hide")
    336                              .append(": " + $nextLink.html());
    337         $('.next-class-link').find('.new').empty();
    338       }
    339     } else {
    340       $('.next-page-link').attr('href', $nextLink.attr('href'))
    341                           .removeClass("hide");
    342       // for the footer link, also add the next page title
    343       $('.content-footer .next-page-link').append(": " + $nextLink.html());
    344     }
    345 
    346     if (!startClass && $prevLink.length) {
    347       var prevHref = $prevLink.attr('href');
    348       if (prevHref == SITE_ROOT + 'index.html') {
    349         // Don't show Previous when it leads to the homepage
    350       } else {
    351         $('.prev-page-link').attr('href', $prevLink.attr('href')).removeClass("hide");
    352       }
    353     }
    354 
    355   }
    356 
    357 
    358 
    359   // Set up the course landing pages for Training with class names and descriptions
    360   if ($('body.trainingcourse').length) {
    361     var $classLinks = $selListItem.find('ul li a').not('#nav .nav-section .nav-section ul a');
    362 
    363     // create an array for all the class descriptions
    364     var $classDescriptions = new Array($classLinks.length);
    365     var lang = getLangPref();
    366     $classLinks.each(function(index) {
    367       var langDescr = $(this).attr(lang + "-description");
    368       if (typeof langDescr !== 'undefined' && langDescr !== false) {
    369         // if there's a class description in the selected language, use that
    370         $classDescriptions[index] = langDescr;
    371       } else {
    372         // otherwise, use the default english description
    373         $classDescriptions[index] = $(this).attr("description");
    374       }
    375     });
    376 
    377     var $olClasses  = $('<ol class="class-list"></ol>');
    378     var $liClass;
    379     var $h2Title;
    380     var $pSummary;
    381     var $olLessons;
    382     var $liLesson;
    383     $classLinks.each(function(index) {
    384       $liClass  = $('<li class="clearfix"></li>');
    385       $h2Title  = $('<a class="title" href="'+$(this).attr('href')+'"><h2 class="norule">' + $(this).html()+'</h2><span></span></a>');
    386       $pSummary = $('<p class="description">' + $classDescriptions[index] + '</p>');
    387 
    388       $olLessons  = $('<ol class="lesson-list"></ol>');
    389 
    390       $lessons = $(this).closest('li').find('ul li a');
    391 
    392       if ($lessons.length) {
    393         $lessons.each(function(index) {
    394           $olLessons.append('<li><a href="'+$(this).attr('href')+'">' + $(this).html()+'</a></li>');
    395         });
    396       } else {
    397         $pSummary.addClass('article');
    398       }
    399 
    400       $liClass.append($h2Title).append($pSummary).append($olLessons);
    401       $olClasses.append($liClass);
    402     });
    403     $('.jd-descr').append($olClasses);
    404   }
    405 
    406   // Set up expand/collapse behavior
    407   initExpandableNavItems("#nav");
    408 
    409 
    410   $(".scroll-pane").scroll(function(event) {
    411       event.preventDefault();
    412       return false;
    413   });
    414 
    415   /* Resize nav height when window height changes */
    416   $(window).resize(function() {
    417     if ($('#side-nav').length == 0) return;
    418     setNavBarDimensions(); // do this even if sidenav isn't fixed because it could become fixed
    419     // make sidenav behave when resizing the window and side-scolling is a concern
    420     updateSideNavDimensions();
    421     checkSticky();
    422     resizeNav(250);
    423   });
    424 
    425   if ($('#devdoc-nav').length) {
    426     setNavBarDimensions();
    427   }
    428 
    429 
    430   // Set up play-on-hover <video> tags.
    431   $('video.play-on-hover').bind('click', function(){
    432     $(this).get(0).load(); // in case the video isn't seekable
    433     $(this).get(0).play();
    434   });
    435 
    436   // Set up tooltips
    437   var TOOLTIP_MARGIN = 10;
    438   $('acronym,.tooltip-link').each(function() {
    439     var $target = $(this);
    440     var $tooltip = $('<div>')
    441         .addClass('tooltip-box')
    442         .append($target.attr('title'))
    443         .hide()
    444         .appendTo('body');
    445     $target.removeAttr('title');
    446 
    447     $target.hover(function() {
    448       // in
    449       var targetRect = $target.offset();
    450       targetRect.width = $target.width();
    451       targetRect.height = $target.height();
    452 
    453       $tooltip.css({
    454         left: targetRect.left,
    455         top: targetRect.top + targetRect.height + TOOLTIP_MARGIN
    456       });
    457       $tooltip.addClass('below');
    458       $tooltip.show();
    459     }, function() {
    460       // out
    461       $tooltip.hide();
    462     });
    463   });
    464 
    465   // Set up <h2> deeplinks
    466   $('h2').click(function() {
    467     var id = $(this).attr('id');
    468     if (id) {
    469       if (history && history.replaceState) {
    470         // Change url without scrolling.
    471         history.replaceState({}, '', '#' + id);
    472       } else {
    473         document.location.hash = id;
    474       }
    475     }
    476   });
    477 
    478   //Loads the +1 button
    479   var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
    480   po.src = 'https://apis.google.com/js/plusone.js';
    481   var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
    482 
    483   $(".scroll-pane").removeAttr("tabindex"); // get rid of tabindex added by jscroller
    484 
    485   if ($(".scroll-pane").length > 1) {
    486     // Check if there's a user preference for the panel heights
    487     var cookieHeight = readCookie("reference_height");
    488     if (cookieHeight) {
    489       restoreHeight(cookieHeight);
    490     }
    491   }
    492 
    493   // Resize once loading is finished
    494   resizeNav();
    495   // Check if there's an anchor that we need to scroll into view.
    496   // A delay is needed, because some browsers do not immediately scroll down to the anchor
    497   window.setTimeout(offsetScrollForSticky, 100);
    498 
    499   /* init the language selector based on user cookie for lang */
    500   loadLangPref();
    501   changeNavLang(getLangPref());
    502 
    503   /* setup event handlers to ensure the overflow menu is visible while picking lang */
    504   $("#language select")
    505       .mousedown(function() {
    506         $("div.morehover").addClass("hover"); })
    507       .blur(function() {
    508         $("div.morehover").removeClass("hover"); });
    509 
    510   /* some global variable setup */
    511   resizePackagesNav = $("#resize-packages-nav");
    512   classesNav = $("#classes-nav");
    513   devdocNav = $("#devdoc-nav");
    514 
    515   var cookiePath = "";
    516   if (location.href.indexOf("/reference/") != -1) {
    517     cookiePath = "reference_";
    518   } else if (location.href.indexOf("/guide/") != -1) {
    519     cookiePath = "guide_";
    520   } else if (location.href.indexOf("/tools/") != -1) {
    521     cookiePath = "tools_";
    522   } else if (location.href.indexOf("/training/") != -1) {
    523     cookiePath = "training_";
    524   } else if (location.href.indexOf("/design/") != -1) {
    525     cookiePath = "design_";
    526   } else if (location.href.indexOf("/distribute/") != -1) {
    527     cookiePath = "distribute_";
    528   }
    529 
    530 
    531   /* setup shadowbox for any videos that want it */
    532   var $videoLinks = $("a.video-shadowbox-button, a.notice-developers-video");
    533   if ($videoLinks.length) {
    534     // if there's at least one, add the shadowbox HTML to the body
    535     $('body').prepend(
    536 '<div id="video-container">'+
    537   '<div id="video-frame">'+
    538     '<div class="video-close">'+
    539       '<span id="icon-video-close" onclick="closeVideo()">&nbsp;</span>'+
    540     '</div>'+
    541     '<div id="youTubePlayer"></div>'+
    542   '</div>'+
    543 '</div>');
    544 
    545     // loads the IFrame Player API code asynchronously.
    546     $.getScript("https://www.youtube.com/iframe_api");
    547 
    548     $videoLinks.each(function() {
    549       var videoId = $(this).attr('href').split('?v=')[1];
    550       $(this).click(function(event) {
    551         event.preventDefault();
    552         startYouTubePlayer(videoId);
    553       });
    554     });
    555   }
    556 });
    557 // END of the onload event
    558 
    559 
    560 var youTubePlayer;
    561 function onYouTubeIframeAPIReady() {
    562 }
    563 
    564 /* Returns the height the shadowbox video should be. It's based on the current
    565    height of the "video-frame" element, which is 100% height for the window.
    566    Then minus the margin so the video isn't actually the full window height. */
    567 function getVideoHeight() {
    568   var frameHeight = $("#video-frame").height();
    569   var marginTop = $("#video-frame").css('margin-top').split('px')[0];
    570   return frameHeight - (marginTop * 2);
    571 }
    572 
    573 var mPlayerPaused = false;
    574 
    575 function startYouTubePlayer(videoId) {
    576   $("#video-container").show();
    577   $("#video-frame").show();
    578   mPlayerPaused = false;
    579 
    580   // compute the size of the player so it's centered in window
    581   var maxWidth = 940;  // the width of the web site content
    582   var videoAspect = .5625; // based on 1280x720 resolution
    583   var maxHeight = maxWidth * videoAspect;
    584   var videoHeight = getVideoHeight();
    585   var videoWidth = videoHeight / videoAspect;
    586   if (videoWidth > maxWidth) {
    587     videoWidth = maxWidth;
    588     videoHeight = maxHeight;
    589   }
    590   $("#video-frame").css('width', videoWidth);
    591 
    592   // check if we've already created this player
    593   if (youTubePlayer == null) {
    594     // check if there's a start time specified
    595     var idAndHash = videoId.split("#");
    596     var startTime = 0;
    597     if (idAndHash.length > 1) {
    598       startTime = idAndHash[1].split("t=")[1] != undefined ? idAndHash[1].split("t=")[1] : 0;
    599     }
    600     // enable localized player
    601     var lang = getLangPref();
    602     var captionsOn = lang == 'en' ? 0 : 1;
    603 
    604     youTubePlayer = new YT.Player('youTubePlayer', {
    605       height: videoHeight,
    606       width: videoWidth,
    607       videoId: idAndHash[0],
    608       playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn},
    609       events: {
    610         'onReady': onPlayerReady,
    611         'onStateChange': onPlayerStateChange
    612       }
    613     });
    614   } else {
    615     // reset the size in case the user adjusted the window since last play
    616     youTubePlayer.setSize(videoWidth, videoHeight);
    617     // if a video different from the one already playing was requested, cue it up
    618     if (videoId != youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0]) {
    619       youTubePlayer.cueVideoById(videoId);
    620     }
    621     youTubePlayer.playVideo();
    622   }
    623 }
    624 
    625 function onPlayerReady(event) {
    626   event.target.playVideo();
    627   mPlayerPaused = false;
    628 }
    629 
    630 function closeVideo() {
    631   try {
    632     youTubePlayer.pauseVideo();
    633   } catch(e) {
    634   }
    635   $("#video-container").fadeOut(200);
    636 }
    637 
    638 /* Track youtube playback for analytics */
    639 function onPlayerStateChange(event) {
    640     // Video starts, send the video ID
    641     if (event.data == YT.PlayerState.PLAYING) {
    642       if (mPlayerPaused) {
    643         ga('send', 'event', 'Videos', 'Resume',
    644             youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0]);
    645       } else {
    646         // track the start playing event so we know from which page the video was selected
    647         ga('send', 'event', 'Videos', 'Start: ' +
    648             youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0],
    649             'on: ' + document.location.href);
    650       }
    651       mPlayerPaused = false;
    652     }
    653     // Video paused, send video ID and video elapsed time
    654     if (event.data == YT.PlayerState.PAUSED) {
    655       ga('send', 'event', 'Videos', 'Paused',
    656             youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0],
    657             youTubePlayer.getCurrentTime());
    658       mPlayerPaused = true;
    659     }
    660     // Video finished, send video ID and video elapsed time
    661     if (event.data == YT.PlayerState.ENDED) {
    662       ga('send', 'event', 'Videos', 'Finished',
    663             youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0],
    664             youTubePlayer.getCurrentTime());
    665       mPlayerPaused = true;
    666     }
    667 }
    668 
    669 
    670 
    671 function initExpandableNavItems(rootTag) {
    672   $(rootTag + ' li.nav-section .nav-section-header').click(function() {
    673     var section = $(this).closest('li.nav-section');
    674     if (section.hasClass('expanded')) {
    675     /* hide me and descendants */
    676       section.find('ul').slideUp(250, function() {
    677         // remove 'expanded' class from my section and any children
    678         section.closest('li').removeClass('expanded');
    679         $('li.nav-section', section).removeClass('expanded');
    680         resizeNav();
    681       });
    682     } else {
    683     /* show me */
    684       // first hide all other siblings
    685       var $others = $('li.nav-section.expanded', $(this).closest('ul')).not('.sticky');
    686       $others.removeClass('expanded').children('ul').slideUp(250);
    687 
    688       // now expand me
    689       section.closest('li').addClass('expanded');
    690       section.children('ul').slideDown(250, function() {
    691         resizeNav();
    692       });
    693     }
    694   });
    695 
    696   // Stop expand/collapse behavior when clicking on nav section links
    697   // (since we're navigating away from the page)
    698   // This selector captures the first instance of <a>, but not those with "#" as the href.
    699   $('.nav-section-header').find('a:eq(0)').not('a[href="#"]').click(function(evt) {
    700     window.location.href = $(this).attr('href');
    701     return false;
    702   });
    703 }
    704 
    705 
    706 /** Create the list of breadcrumb links in the sticky header */
    707 function buildBreadcrumbs() {
    708   var $breadcrumbUl =  $(".dac-header-crumbs");
    709   var primaryNavLink = ".dac-nav-list > .dac-nav-item > .dac-nav-link";
    710 
    711   // Add the secondary horizontal nav item, if provided
    712   var $selectedSecondNav = $(".dac-nav-secondary .dac-nav-link.selected").clone()
    713     .attr('class', 'dac-header-crumbs-link');
    714 
    715   if ($selectedSecondNav.length) {
    716     $breadcrumbUl.prepend($('<li class="dac-header-crumbs-item">').append($selectedSecondNav));
    717   }
    718 
    719   // Add the primary horizontal nav
    720   var $selectedFirstNav = $(primaryNavLink + ".selected, " + primaryNavLink + ".has-subnav").clone()
    721     .attr('class', 'dac-header-crumbs-link');
    722 
    723   // If there's no header nav item, use the logo link and title from alt text
    724   if ($selectedFirstNav.length < 1) {
    725     $selectedFirstNav = $('<a class="dac-header-crumbs-link">')
    726         .attr('href', $("div#header .logo a").attr('href'))
    727         .text($("div#header .logo img").attr('alt'));
    728   }
    729   $breadcrumbUl.prepend($('<li class="dac-header-crumbs-item">').append($selectedFirstNav));
    730 }
    731 
    732 
    733 
    734 /** Highlight the current page in sidenav, expanding children as appropriate */
    735 function highlightSidenav() {
    736   // if something is already highlighted, undo it. This is for dynamic navigation (Samples index)
    737   if ($("ul#nav li.selected").length) {
    738     unHighlightSidenav();
    739   }
    740   // look for URL in sidenav, including the hash
    741   var $selNavLink = $('#nav').find('a[href="' + mPagePath + location.hash + '"]');
    742 
    743   // If the selNavLink is still empty, look for it without the hash
    744   if ($selNavLink.length == 0) {
    745     $selNavLink = $('#nav').find('a[href="' + mPagePath + '"]');
    746   }
    747 
    748   var $selListItem;
    749   if ($selNavLink.length) {
    750     // Find this page's <li> in sidenav and set selected
    751     $selListItem = $selNavLink.closest('li');
    752     $selListItem.addClass('selected');
    753 
    754     // Traverse up the tree and expand all parent nav-sections
    755     $selNavLink.parents('li.nav-section').each(function() {
    756       $(this).addClass('expanded');
    757       $(this).children('ul').show();
    758     });
    759   }
    760 }
    761 
    762 function unHighlightSidenav() {
    763   $("ul#nav li.selected").removeClass("selected");
    764   $('ul#nav li.nav-section.expanded').removeClass('expanded').children('ul').hide();
    765 }
    766 
    767 function toggleFullscreen(enable) {
    768   var delay = 20;
    769   var enabled = true;
    770   var stylesheet = $('link[rel="stylesheet"][class="fullscreen"]');
    771   if (enable) {
    772     // Currently NOT USING fullscreen; enable fullscreen
    773     stylesheet.removeAttr('disabled');
    774     $('#nav-swap .fullscreen').removeClass('disabled');
    775     $('#devdoc-nav').css({left:''});
    776     setTimeout(updateSidenavFullscreenWidth,delay); // need to wait a moment for css to switch
    777     enabled = true;
    778   } else {
    779     // Currently USING fullscreen; disable fullscreen
    780     stylesheet.attr('disabled', 'disabled');
    781     $('#nav-swap .fullscreen').addClass('disabled');
    782     setTimeout(updateSidenavFixedWidth,delay); // need to wait a moment for css to switch
    783     enabled = false;
    784   }
    785   writeCookie("fullscreen", enabled, null);
    786   setNavBarDimensions();
    787   resizeNav(delay);
    788   updateSideNavDimensions();
    789   setTimeout(initSidenavHeightResize,delay);
    790 }
    791 
    792 // TODO: Refactor into a closure.
    793 var navBarLeftPos;
    794 var navBarWidth;
    795 function setNavBarDimensions() {
    796   navBarLeftPos = $('#body-content').offset().left;
    797   navBarWidth = $('#side-nav').width();
    798 }
    799 
    800 
    801 function updateSideNavDimensions() {
    802   var newLeft = $(window).scrollLeft() - navBarLeftPos;
    803   $('#devdoc-nav').css({left: -newLeft, width: navBarWidth});
    804   $('#devdoc-nav .totop').css({left: -(newLeft - parseInt($('#side-nav').css('padding-left')))});
    805 }
    806 
    807 // TODO: use $(document).ready instead
    808 function addLoadEvent(newfun) {
    809   var current = window.onload;
    810   if (typeof window.onload != 'function') {
    811     window.onload = newfun;
    812   } else {
    813     window.onload = function() {
    814       current();
    815       newfun();
    816     }
    817   }
    818 }
    819 
    820 var agent = navigator['userAgent'].toLowerCase();
    821 // If a mobile phone, set flag and do mobile setup
    822 if ((agent.indexOf("mobile") != -1) ||      // android, iphone, ipod
    823     (agent.indexOf("blackberry") != -1) ||
    824     (agent.indexOf("webos") != -1) ||
    825     (agent.indexOf("mini") != -1)) {        // opera mini browsers
    826   isMobile = true;
    827 }
    828 
    829 
    830 $(document).ready(function() {
    831   $("pre:not(.no-pretty-print)").addClass("prettyprint");
    832   prettyPrint();
    833 });
    834 
    835 
    836 
    837 
    838 /* ######### RESIZE THE SIDENAV ########## */
    839 
    840 function resizeNav(delay) {
    841   var $nav = $("#devdoc-nav");
    842   var $window = $(window);
    843   var navHeight;
    844 
    845   // Get the height of entire window and the total header height.
    846   // Then figure out based on scroll position whether the header is visible
    847   var windowHeight = $window.height();
    848   var scrollTop = $window.scrollTop();
    849   var headerHeight = $('#header-wrapper').outerHeight();
    850   var headerVisible = scrollTop < stickyTop;
    851 
    852   // get the height of space between nav and top of window.
    853   // Could be either margin or top position, depending on whether the nav is fixed.
    854   var topMargin = (parseInt($nav.css('top')) || 20) + 1;
    855   // add 1 for the #side-nav bottom margin
    856 
    857   // Depending on whether the header is visible, set the side nav's height.
    858   if (headerVisible) {
    859     // The sidenav height grows as the header goes off screen
    860     navHeight = windowHeight - (headerHeight - scrollTop) - topMargin;
    861   } else {
    862     // Once header is off screen, the nav height is almost full window height
    863     navHeight = windowHeight - topMargin;
    864   }
    865 
    866 
    867 
    868   $scrollPanes = $(".scroll-pane");
    869   if ($window.width() < 720) {
    870     $nav.css('height', '');
    871   } else if ($scrollPanes.length > 1) {
    872     // subtract the height of the api level widget and nav swapper from the available nav height
    873     navHeight -= ($('#api-nav-header').outerHeight(true) + $('#nav-swap').outerHeight(true));
    874 
    875     $("#swapper").css({height:navHeight + "px"});
    876     if ($("#nav-tree").is(":visible")) {
    877       $("#nav-tree").css({height:navHeight});
    878     }
    879 
    880     var classesHeight = navHeight - parseInt($("#resize-packages-nav").css("height")) - 10 + "px";
    881     //subtract 10px to account for drag bar
    882 
    883     // if the window becomes small enough to make the class panel height 0,
    884     // then the package panel should begin to shrink
    885     if (parseInt(classesHeight) <= 0) {
    886       $("#resize-packages-nav").css({height:navHeight - 10}); //subtract 10px for drag bar
    887       $("#packages-nav").css({height:navHeight - 10});
    888     }
    889 
    890     $("#classes-nav").css({'height':classesHeight, 'margin-top':'10px'});
    891     $("#classes-nav .jspContainer").css({height:classesHeight});
    892 
    893 
    894   } else {
    895     $nav.height(navHeight);
    896   }
    897 
    898   if (delay) {
    899     updateFromResize = true;
    900     delayedReInitScrollbars(delay);
    901   } else {
    902     reInitScrollbars();
    903   }
    904 
    905 }
    906 
    907 var updateScrollbars = false;
    908 var updateFromResize = false;
    909 
    910 /* Re-initialize the scrollbars to account for changed nav size.
    911  * This method postpones the actual update by a 1/4 second in order to optimize the
    912  * scroll performance while the header is still visible, because re-initializing the
    913  * scroll panes is an intensive process.
    914  */
    915 function delayedReInitScrollbars(delay) {
    916   // If we're scheduled for an update, but have received another resize request
    917   // before the scheduled resize has occured, just ignore the new request
    918   // (and wait for the scheduled one).
    919   if (updateScrollbars && updateFromResize) {
    920     updateFromResize = false;
    921     return;
    922   }
    923 
    924   // We're scheduled for an update and the update request came from this method's setTimeout
    925   if (updateScrollbars && !updateFromResize) {
    926     reInitScrollbars();
    927     updateScrollbars = false;
    928   } else {
    929     updateScrollbars = true;
    930     updateFromResize = false;
    931     setTimeout('delayedReInitScrollbars()',delay);
    932   }
    933 }
    934 
    935 /* Re-initialize the scrollbars to account for changed nav size. */
    936 function reInitScrollbars() {
    937   var pane = $(".scroll-pane").each(function(){
    938     var api = $(this).data('jsp');
    939     if (!api) {return;}
    940     api.reinitialise( {verticalGutter:0} );
    941   });
    942   $(".scroll-pane").removeAttr("tabindex"); // get rid of tabindex added by jscroller
    943 }
    944 
    945 
    946 /* Resize the height of the nav panels in the reference,
    947  * and save the new size to a cookie */
    948 function saveNavPanels() {
    949   var basePath = getBaseUri(location.pathname);
    950   var section = basePath.substring(1,basePath.indexOf("/",1));
    951   writeCookie("height", resizePackagesNav.css("height"), section);
    952 }
    953 
    954 
    955 
    956 function restoreHeight(packageHeight) {
    957     $("#resize-packages-nav").height(packageHeight);
    958     $("#packages-nav").height(packageHeight);
    959   //  var classesHeight = navHeight - packageHeight;
    960  //   $("#classes-nav").css({height:classesHeight});
    961   //  $("#classes-nav .jspContainer").css({height:classesHeight});
    962 }
    963 
    964 
    965 
    966 /* ######### END RESIZE THE SIDENAV HEIGHT ########## */
    967 
    968 
    969 
    970 
    971 
    972 /** Scroll the jScrollPane to make the currently selected item visible
    973     This is called when the page finished loading. */
    974 function scrollIntoView(nav) {
    975   return;
    976   var $nav = $("#"+nav);
    977   var element = $nav.jScrollPane({/* ...settings... */});
    978   var api = element.data('jsp');
    979 
    980   if ($nav.is(':visible')) {
    981     var $selected = $(".selected", $nav);
    982     if ($selected.length == 0) {
    983       // If no selected item found, exit
    984       return;
    985     }
    986     // get the selected item's offset from its container nav by measuring the item's offset
    987     // relative to the document then subtract the container nav's offset relative to the document
    988     var selectedOffset = $selected.offset().top - $nav.offset().top + 60;
    989     if (selectedOffset > $nav.height() * .8) { // multiply nav height by .8 so we move up the item
    990                                                // if it's more than 80% down the nav
    991       // scroll the item up by an amount equal to 80% the container nav's height
    992       api.scrollTo(0, selectedOffset - ($nav.height() * .8), false);
    993     }
    994   }
    995 }
    996 
    997 
    998 
    999 
   1000 
   1001 
   1002 /* Show popup dialogs */
   1003 function showDialog(id) {
   1004   $dialog = $("#"+id);
   1005   $dialog.prepend('<div class="box-border"><div class="top"> <div class="left"></div> <div class="right"></div></div><div class="bottom"> <div class="left"></div> <div class="right"></div> </div> </div>');
   1006   $dialog.wrapInner('<div/>');
   1007   $dialog.removeClass("hide");
   1008 }
   1009 
   1010 
   1011 
   1012 
   1013 
   1014 /* #########    COOKIES!     ########## */
   1015 
   1016 function readCookie(cookie) {
   1017   var myCookie = cookie_namespace+"_"+cookie+"=";
   1018   if (document.cookie) {
   1019     var index = document.cookie.indexOf(myCookie);
   1020     if (index != -1) {
   1021       var valStart = index + myCookie.length;
   1022       var valEnd = document.cookie.indexOf(";", valStart);
   1023       if (valEnd == -1) {
   1024         valEnd = document.cookie.length;
   1025       }
   1026       var val = document.cookie.substring(valStart, valEnd);
   1027       return val;
   1028     }
   1029   }
   1030   return 0;
   1031 }
   1032 
   1033 function writeCookie(cookie, val, section) {
   1034   if (val==undefined) return;
   1035   section = section == null ? "_" : "_"+section+"_";
   1036   var age = 2*365*24*60*60; // set max-age to 2 years
   1037   var cookieValue = cookie_namespace + section + cookie + "=" + val
   1038                     + "; max-age=" + age +"; path=/";
   1039   document.cookie = cookieValue;
   1040 }
   1041 
   1042 /* #########     END COOKIES!     ########## */
   1043 
   1044 
   1045 var sticky = false;
   1046 var stickyTop;
   1047 var prevScrollLeft = 0; // used to compare current position to previous position of horiz scroll
   1048 /* Sets the vertical scoll position at which the sticky bar should appear.
   1049    This method is called to reset the position when search results appear or hide */
   1050 function setStickyTop() {
   1051   stickyTop = $('#header-wrapper').outerHeight() - $('#header > .dac-header-inner').outerHeight();
   1052 }
   1053 
   1054 /*
   1055  * Displays sticky nav bar on pages when dac header scrolls out of view
   1056  */
   1057 $(window).scroll(function(event) {
   1058   // Exit if the mouse target is a DIV, because that means the event is coming
   1059   // from a scrollable div and so there's no need to make adjustments to our layout
   1060   if ($(event.target).nodeName == "DIV") {
   1061     return;
   1062   }
   1063 
   1064   checkSticky();
   1065 });
   1066 
   1067 function checkSticky() {
   1068   setStickyTop();
   1069   var $headerEl = $('#header');
   1070   // Exit if there's no sidenav
   1071   if ($('#side-nav').length == 0) return;
   1072 
   1073   var top = $(window).scrollTop();
   1074   // we set the navbar fixed when the scroll position is beyond the height of the site header...
   1075   var shouldBeSticky = top > stickyTop;
   1076   // ... except if the document content is shorter than the sidenav height.
   1077   // (this is necessary to avoid crazy behavior on OSX Lion due to overscroll bouncing)
   1078   if ($("#doc-col").height() < $("#side-nav").height()) {
   1079     shouldBeSticky = false;
   1080   }
   1081   // Nor on mobile
   1082   if (window.innerWidth < 720) {
   1083     shouldBeSticky = false;
   1084   }
   1085   // Account for horizontal scroll
   1086   var scrollLeft = $(window).scrollLeft();
   1087   // When the sidenav is fixed and user scrolls horizontally, reposition the sidenav to match
   1088   if (sticky && (scrollLeft != prevScrollLeft)) {
   1089     updateSideNavDimensions();
   1090     prevScrollLeft = scrollLeft;
   1091   }
   1092 
   1093   // Don't continue if the header is sufficently far away
   1094   // (to avoid intensive resizing that slows scrolling)
   1095   if (sticky == shouldBeSticky) {
   1096     return;
   1097   }
   1098 
   1099   // If sticky header visible and position is now near top, hide sticky
   1100   if (sticky && !shouldBeSticky) {
   1101     sticky = false;
   1102     // make the sidenav static again
   1103     $('#devdoc-nav')
   1104       .removeClass('fixed')
   1105       .css({'width':'auto','margin':''});
   1106     // delay hide the sticky
   1107     $headerEl.removeClass('is-sticky');
   1108 
   1109     // update the sidenaav position for side scrolling
   1110     updateSideNavDimensions();
   1111   } else if (!sticky && shouldBeSticky) {
   1112     sticky = true;
   1113     $headerEl.addClass('is-sticky');
   1114 
   1115     // make the sidenav fixed
   1116     $('#devdoc-nav')
   1117       .addClass('fixed');
   1118 
   1119     // update the sidenaav position for side scrolling
   1120     updateSideNavDimensions();
   1121 
   1122   }
   1123   resizeNav(250); // pass true in order to delay the scrollbar re-initialization for performance
   1124 }
   1125 
   1126 /*
   1127  * Manages secion card states and nav resize to conclude loading
   1128  */
   1129 (function() {
   1130   $(document).ready(function() {
   1131 
   1132     // Stack hover states
   1133     $('.section-card-menu').each(function(index, el) {
   1134       var height = $(el).height();
   1135       $(el).css({height:height+'px', position:'relative'});
   1136       var $cardInfo = $(el).find('.card-info');
   1137 
   1138       $cardInfo.css({position: 'absolute', bottom:'0px', left:'0px', right:'0px', overflow:'visible'});
   1139     });
   1140 
   1141   });
   1142 
   1143 })();
   1144 
   1145 
   1146 
   1147 
   1148 
   1149 
   1150 
   1151 
   1152 
   1153 
   1154 
   1155 
   1156 
   1157 
   1158 /*      MISC LIBRARY FUNCTIONS     */
   1159 
   1160 
   1161 
   1162 
   1163 
   1164 function toggle(obj, slide) {
   1165   var ul = $("ul:first", obj);
   1166   var li = ul.parent();
   1167   if (li.hasClass("closed")) {
   1168     if (slide) {
   1169       ul.slideDown("fast");
   1170     } else {
   1171       ul.show();
   1172     }
   1173     li.removeClass("closed");
   1174     li.addClass("open");
   1175     $(".toggle-img", li).attr("title", "hide pages");
   1176   } else {
   1177     ul.slideUp("fast");
   1178     li.removeClass("open");
   1179     li.addClass("closed");
   1180     $(".toggle-img", li).attr("title", "show pages");
   1181   }
   1182 }
   1183 
   1184 
   1185 function buildToggleLists() {
   1186   $(".toggle-list").each(
   1187     function(i) {
   1188       $("div:first", this).append("<a class='toggle-img' href='#' title='show pages' onClick='toggle(this.parentNode.parentNode, true); return false;'></a>");
   1189       $(this).addClass("closed");
   1190     });
   1191 }
   1192 
   1193 
   1194 
   1195 function hideNestedItems(list, toggle) {
   1196   $list = $(list);
   1197   // hide nested lists
   1198   if($list.hasClass('showing')) {
   1199     $("li ol", $list).hide('fast');
   1200     $list.removeClass('showing');
   1201   // show nested lists
   1202   } else {
   1203     $("li ol", $list).show('fast');
   1204     $list.addClass('showing');
   1205   }
   1206   $(".more,.less",$(toggle)).toggle();
   1207 }
   1208 
   1209 
   1210 /* Call this to add listeners to a <select> element for Studio/Eclipse/Other docs */
   1211 function setupIdeDocToggle() {
   1212   $( "select.ide" ).change(function() {
   1213     var selected = $(this).find("option:selected").attr("value");
   1214     $(".select-ide").hide();
   1215     $(".select-ide."+selected).show();
   1216 
   1217     $("select.ide").val(selected);
   1218   });
   1219 }
   1220 
   1221 
   1222 
   1223 
   1224 
   1225 
   1226 
   1227 
   1228 
   1229 
   1230 
   1231 
   1232 
   1233 
   1234 
   1235 
   1236 
   1237 
   1238 
   1239 
   1240 
   1241 
   1242 
   1243 
   1244 /*      REFERENCE NAV SWAP     */
   1245 
   1246 
   1247 function getNavPref() {
   1248   var v = readCookie('reference_nav');
   1249   if (v != NAV_PREF_TREE) {
   1250     v = NAV_PREF_PANELS;
   1251   }
   1252   return v;
   1253 }
   1254 
   1255 function chooseDefaultNav() {
   1256   nav_pref = getNavPref();
   1257   if (nav_pref == NAV_PREF_TREE) {
   1258     $("#nav-panels").toggle();
   1259     $("#panel-link").toggle();
   1260     $("#nav-tree").toggle();
   1261     $("#tree-link").toggle();
   1262   }
   1263 }
   1264 
   1265 function swapNav() {
   1266   if (nav_pref == NAV_PREF_TREE) {
   1267     nav_pref = NAV_PREF_PANELS;
   1268   } else {
   1269     nav_pref = NAV_PREF_TREE;
   1270     init_default_navtree(toRoot);
   1271   }
   1272   writeCookie("nav", nav_pref, "reference");
   1273 
   1274   $("#nav-panels").toggle();
   1275   $("#panel-link").toggle();
   1276   $("#nav-tree").toggle();
   1277   $("#tree-link").toggle();
   1278 
   1279   resizeNav();
   1280 
   1281   // Gross nasty hack to make tree view show up upon first swap by setting height manually
   1282   $("#nav-tree .jspContainer:visible")
   1283       .css({'height':$("#nav-tree .jspContainer .jspPane").height() +'px'});
   1284   // Another nasty hack to make the scrollbar appear now that we have height
   1285   resizeNav();
   1286 
   1287   if ($("#nav-tree").is(':visible')) {
   1288     scrollIntoView("nav-tree");
   1289   } else {
   1290     scrollIntoView("packages-nav");
   1291     scrollIntoView("classes-nav");
   1292   }
   1293 }
   1294 
   1295 
   1296 
   1297 /* ############################################ */
   1298 /* ##########     LOCALIZATION     ############ */
   1299 /* ############################################ */
   1300 
   1301 function getBaseUri(uri) {
   1302   var intlUrl = (uri.substring(0,6) == "/intl/");
   1303   if (intlUrl) {
   1304     base = uri.substring(uri.indexOf('intl/')+5,uri.length);
   1305     base = base.substring(base.indexOf('/')+1, base.length);
   1306       //alert("intl, returning base url: /" + base);
   1307     return ("/" + base);
   1308   } else {
   1309       //alert("not intl, returning uri as found.");
   1310     return uri;
   1311   }
   1312 }
   1313 
   1314 function requestAppendHL(uri) {
   1315 //append "?hl=<lang> to an outgoing request (such as to blog)
   1316   var lang = getLangPref();
   1317   if (lang) {
   1318     var q = 'hl=' + lang;
   1319     uri += '?' + q;
   1320     window.location = uri;
   1321     return false;
   1322   } else {
   1323     return true;
   1324   }
   1325 }
   1326 
   1327 
   1328 function changeNavLang(lang) {
   1329   if (lang === 'en') { return; }
   1330 
   1331   var $links = $("a[" + lang + "-lang],p[" + lang + "-lang]");
   1332   $links.each(function(){ // for each link with a translation
   1333     var $link = $(this);
   1334     // put the desired language from the attribute as the text
   1335     $link.text($link.attr(lang + '-lang'))
   1336   });
   1337 }
   1338 
   1339 function changeLangPref(lang, submit) {
   1340   writeCookie("pref_lang", lang, null);
   1341 
   1342   //  #######  TODO:  Remove this condition once we're stable on devsite #######
   1343   //  This condition is only needed if we still need to support legacy GAE server
   1344   if (devsite) {
   1345     // Switch language when on Devsite server
   1346     if (submit) {
   1347       $("#setlang").submit();
   1348     }
   1349   } else {
   1350     // Switch language when on legacy GAE server
   1351     if (submit) {
   1352       window.location = getBaseUri(location.pathname);
   1353     }
   1354   }
   1355 }
   1356 
   1357 function loadLangPref() {
   1358   var lang = readCookie("pref_lang");
   1359   if (lang != 0) {
   1360     $("#language").find("option[value='"+lang+"']").attr("selected",true);
   1361   }
   1362 }
   1363 
   1364 function getLangPref() {
   1365   var lang = $("#language").find(":selected").attr("value");
   1366   if (!lang) {
   1367     lang = readCookie("pref_lang");
   1368   }
   1369   return (lang != 0) ? lang : 'en';
   1370 }
   1371 
   1372 /* ##########     END LOCALIZATION     ############ */
   1373 
   1374 
   1375 
   1376 
   1377 
   1378 
   1379 /* Used to hide and reveal supplemental content, such as long code samples.
   1380    See the companion CSS in android-developer-docs.css */
   1381 function toggleContent(obj) {
   1382   var div = $(obj).closest(".toggle-content");
   1383   var toggleMe = $(".toggle-content-toggleme:eq(0)",div);
   1384   if (div.hasClass("closed")) { // if it's closed, open it
   1385     toggleMe.slideDown();
   1386     $(".toggle-content-text:eq(0)", obj).toggle();
   1387     div.removeClass("closed").addClass("open");
   1388     $(".toggle-content-img:eq(0)", div).attr("title", "hide").attr("src", toRoot
   1389                   + "assets/images/styles/disclosure_up.png");
   1390   } else { // if it's open, close it
   1391     toggleMe.slideUp('fast', function() {  // Wait until the animation is done before closing arrow
   1392       $(".toggle-content-text:eq(0)", obj).toggle();
   1393       div.removeClass("open").addClass("closed");
   1394       div.find(".toggle-content").removeClass("open").addClass("closed")
   1395               .find(".toggle-content-toggleme").hide();
   1396       $(".toggle-content-img", div).attr("title", "show").attr("src", toRoot
   1397                   + "assets/images/styles/disclosure_down.png");
   1398     });
   1399   }
   1400   return false;
   1401 }
   1402 
   1403 
   1404 /* New version of expandable content */
   1405 function toggleExpandable(link,id) {
   1406   if($(id).is(':visible')) {
   1407     $(id).slideUp();
   1408     $(link).removeClass('expanded');
   1409   } else {
   1410     $(id).slideDown();
   1411     $(link).addClass('expanded');
   1412   }
   1413 }
   1414 
   1415 function hideExpandable(ids) {
   1416   $(ids).slideUp();
   1417   $(ids).prev('h4').find('a.expandable').removeClass('expanded');
   1418 }
   1419 
   1420 
   1421 
   1422 
   1423 
   1424 /*
   1425  *  Slideshow 1.0
   1426  *  Used on /index.html and /develop/index.html for carousel
   1427  *
   1428  *  Sample usage:
   1429  *  HTML -
   1430  *  <div class="slideshow-container">
   1431  *   <a href="" class="slideshow-prev">Prev</a>
   1432  *   <a href="" class="slideshow-next">Next</a>
   1433  *   <ul>
   1434  *       <li class="item"><img src="images/marquee1.jpg"></li>
   1435  *       <li class="item"><img src="images/marquee2.jpg"></li>
   1436  *       <li class="item"><img src="images/marquee3.jpg"></li>
   1437  *       <li class="item"><img src="images/marquee4.jpg"></li>
   1438  *   </ul>
   1439  *  </div>
   1440  *
   1441  *   <script type="text/javascript">
   1442  *   $('.slideshow-container').dacSlideshow({
   1443  *       auto: true,
   1444  *       btnPrev: '.slideshow-prev',
   1445  *       btnNext: '.slideshow-next'
   1446  *   });
   1447  *   </script>
   1448  *
   1449  *  Options:
   1450  *  btnPrev:    optional identifier for previous button
   1451  *  btnNext:    optional identifier for next button
   1452  *  btnPause:   optional identifier for pause button
   1453  *  auto:       whether or not to auto-proceed
   1454  *  speed:      animation speed
   1455  *  autoTime:   time between auto-rotation
   1456  *  easing:     easing function for transition
   1457  *  start:      item to select by default
   1458  *  scroll:     direction to scroll in
   1459  *  pagination: whether or not to include dotted pagination
   1460  *
   1461  */
   1462 
   1463  (function($) {
   1464  $.fn.dacSlideshow = function(o) {
   1465 
   1466      //Options - see above
   1467      o = $.extend({
   1468          btnPrev:   null,
   1469          btnNext:   null,
   1470          btnPause:  null,
   1471          auto:      true,
   1472          speed:     500,
   1473          autoTime:  12000,
   1474          easing:    null,
   1475          start:     0,
   1476          scroll:    1,
   1477          pagination: true
   1478 
   1479      }, o || {});
   1480 
   1481      //Set up a carousel for each
   1482      return this.each(function() {
   1483 
   1484          var running = false;
   1485          var animCss = o.vertical ? "top" : "left";
   1486          var sizeCss = o.vertical ? "height" : "width";
   1487          var div = $(this);
   1488          var ul = $("ul", div);
   1489          var tLi = $("li", ul);
   1490          var tl = tLi.size();
   1491          var timer = null;
   1492 
   1493          var li = $("li", ul);
   1494          var itemLength = li.size();
   1495          var curr = o.start;
   1496 
   1497          li.css({float: o.vertical ? "none" : "left"});
   1498          ul.css({margin: "0", padding: "0", position: "relative", "list-style-type": "none", "z-index": "1"});
   1499          div.css({position: "relative", "z-index": "2", left: "0px"});
   1500 
   1501          var liSize = o.vertical ? height(li) : width(li);
   1502          var ulSize = liSize * itemLength;
   1503          var divSize = liSize;
   1504 
   1505          li.css({width: li.width(), height: li.height()});
   1506          ul.css(sizeCss, ulSize+"px").css(animCss, -(curr*liSize));
   1507 
   1508          div.css(sizeCss, divSize+"px");
   1509 
   1510          //Pagination
   1511          if (o.pagination) {
   1512              var pagination = $("<div class='pagination'></div>");
   1513              var pag_ul = $("<ul></ul>");
   1514              if (tl > 1) {
   1515                for (var i=0;i<tl;i++) {
   1516                     var li = $("<li>"+i+"</li>");
   1517                     pag_ul.append(li);
   1518                     if (i==o.start) li.addClass('active');
   1519                         li.click(function() {
   1520                         go(parseInt($(this).text()));
   1521                     })
   1522                 }
   1523                 pagination.append(pag_ul);
   1524                 div.append(pagination);
   1525              }
   1526          }
   1527 
   1528          //Previous button
   1529          if(o.btnPrev)
   1530              $(o.btnPrev).click(function(e) {
   1531                  e.preventDefault();
   1532                  return go(curr-o.scroll);
   1533              });
   1534 
   1535          //Next button
   1536          if(o.btnNext)
   1537              $(o.btnNext).click(function(e) {
   1538                  e.preventDefault();
   1539                  return go(curr+o.scroll);
   1540              });
   1541 
   1542          //Pause button
   1543          if(o.btnPause)
   1544              $(o.btnPause).click(function(e) {
   1545                  e.preventDefault();
   1546                  if ($(this).hasClass('paused')) {
   1547                      startRotateTimer();
   1548                  } else {
   1549                      pauseRotateTimer();
   1550                  }
   1551              });
   1552 
   1553          //Auto rotation
   1554          if(o.auto) startRotateTimer();
   1555 
   1556          function startRotateTimer() {
   1557              clearInterval(timer);
   1558              timer = setInterval(function() {
   1559                   if (curr == tl-1) {
   1560                     go(0);
   1561                   } else {
   1562                     go(curr+o.scroll);
   1563                   }
   1564               }, o.autoTime);
   1565              $(o.btnPause).removeClass('paused');
   1566          }
   1567 
   1568          function pauseRotateTimer() {
   1569              clearInterval(timer);
   1570              $(o.btnPause).addClass('paused');
   1571          }
   1572 
   1573          //Go to an item
   1574          function go(to) {
   1575              if(!running) {
   1576 
   1577                  if(to<0) {
   1578                     to = itemLength-1;
   1579                  } else if (to>itemLength-1) {
   1580                     to = 0;
   1581                  }
   1582                  curr = to;
   1583 
   1584                  running = true;
   1585 
   1586                  ul.animate(
   1587                      animCss == "left" ? { left: -(curr*liSize) } : { top: -(curr*liSize) } , o.speed, o.easing,
   1588                      function() {
   1589                          running = false;
   1590                      }
   1591                  );
   1592 
   1593                  $(o.btnPrev + "," + o.btnNext).removeClass("disabled");
   1594                  $( (curr-o.scroll<0 && o.btnPrev)
   1595                      ||
   1596                     (curr+o.scroll > itemLength && o.btnNext)
   1597                      ||
   1598                     []
   1599                   ).addClass("disabled");
   1600 
   1601 
   1602                  var nav_items = $('li', pagination);
   1603                  nav_items.removeClass('active');
   1604                  nav_items.eq(to).addClass('active');
   1605 
   1606 
   1607              }
   1608              if(o.auto) startRotateTimer();
   1609              return false;
   1610          };
   1611      });
   1612  };
   1613 
   1614  function css(el, prop) {
   1615      return parseInt($.css(el[0], prop)) || 0;
   1616  };
   1617  function width(el) {
   1618      return  el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
   1619  };
   1620  function height(el) {
   1621      return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
   1622  };
   1623 
   1624  })(jQuery);
   1625 
   1626 
   1627 /*
   1628  *  dacSlideshow 1.0
   1629  *  Used on develop/index.html for side-sliding tabs
   1630  *
   1631  *  Sample usage:
   1632  *  HTML -
   1633  *  <div class="slideshow-container">
   1634  *   <a href="" class="slideshow-prev">Prev</a>
   1635  *   <a href="" class="slideshow-next">Next</a>
   1636  *   <ul>
   1637  *       <li class="item"><img src="images/marquee1.jpg"></li>
   1638  *       <li class="item"><img src="images/marquee2.jpg"></li>
   1639  *       <li class="item"><img src="images/marquee3.jpg"></li>
   1640  *       <li class="item"><img src="images/marquee4.jpg"></li>
   1641  *   </ul>
   1642  *  </div>
   1643  *
   1644  *   <script type="text/javascript">
   1645  *   $('.slideshow-container').dacSlideshow({
   1646  *       auto: true,
   1647  *       btnPrev: '.slideshow-prev',
   1648  *       btnNext: '.slideshow-next'
   1649  *   });
   1650  *   </script>
   1651  *
   1652  *  Options:
   1653  *  btnPrev:    optional identifier for previous button
   1654  *  btnNext:    optional identifier for next button
   1655  *  auto:       whether or not to auto-proceed
   1656  *  speed:      animation speed
   1657  *  autoTime:   time between auto-rotation
   1658  *  easing:     easing function for transition
   1659  *  start:      item to select by default
   1660  *  scroll:     direction to scroll in
   1661  *  pagination: whether or not to include dotted pagination
   1662  *
   1663  */
   1664  (function($) {
   1665  $.fn.dacTabbedList = function(o) {
   1666 
   1667      //Options - see above
   1668      o = $.extend({
   1669          speed : 250,
   1670          easing: null,
   1671          nav_id: null,
   1672          frame_id: null
   1673      }, o || {});
   1674 
   1675      //Set up a carousel for each
   1676      return this.each(function() {
   1677 
   1678          var curr = 0;
   1679          var running = false;
   1680          var animCss = "margin-left";
   1681          var sizeCss = "width";
   1682          var div = $(this);
   1683 
   1684          var nav = $(o.nav_id, div);
   1685          var nav_li = $("li", nav);
   1686          var nav_size = nav_li.size();
   1687          var frame = div.find(o.frame_id);
   1688          var content_width = $(frame).find('ul').width();
   1689          //Buttons
   1690          $(nav_li).click(function(e) {
   1691            go($(nav_li).index($(this)));
   1692          })
   1693 
   1694          //Go to an item
   1695          function go(to) {
   1696              if(!running) {
   1697                  curr = to;
   1698                  running = true;
   1699 
   1700                  frame.animate({ 'margin-left' : -(curr*content_width) }, o.speed, o.easing,
   1701                      function() {
   1702                          running = false;
   1703                      }
   1704                  );
   1705 
   1706 
   1707                  nav_li.removeClass('active');
   1708                  nav_li.eq(to).addClass('active');
   1709 
   1710 
   1711              }
   1712              return false;
   1713          };
   1714      });
   1715  };
   1716 
   1717  function css(el, prop) {
   1718      return parseInt($.css(el[0], prop)) || 0;
   1719  };
   1720  function width(el) {
   1721      return  el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
   1722  };
   1723  function height(el) {
   1724      return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
   1725  };
   1726 
   1727  })(jQuery);
   1728 
   1729 
   1730 
   1731 
   1732 
   1733 /* ######################################################## */
   1734 /* ################  SEARCH SUGGESTIONS  ################## */
   1735 /* ######################################################## */
   1736 
   1737 
   1738 
   1739 var gSelectedIndex = -1;  // the index position of currently highlighted suggestion
   1740 var gSelectedColumn = -1;  // which column of suggestion lists is currently focused
   1741 
   1742 var gMatches = new Array();
   1743 var gLastText = "";
   1744 var gInitialized = false;
   1745 var ROW_COUNT_FRAMEWORK = 20;       // max number of results in list
   1746 var gListLength = 0;
   1747 
   1748 
   1749 var gGoogleMatches = new Array();
   1750 var ROW_COUNT_GOOGLE = 15;          // max number of results in list
   1751 var gGoogleListLength = 0;
   1752 
   1753 var gDocsMatches = new Array();
   1754 var ROW_COUNT_DOCS = 100;          // max number of results in list
   1755 var gDocsListLength = 0;
   1756 
   1757 function onSuggestionClick(link) {
   1758   // When user clicks a suggested document, track it
   1759   ga('send', 'event', 'Suggestion Click', 'clicked: ' + $(link).attr('href'),
   1760                 'query: ' + $("#search_autocomplete").val().toLowerCase());
   1761 }
   1762 
   1763 function set_item_selected($li, selected)
   1764 {
   1765     if (selected) {
   1766         $li.attr('class','jd-autocomplete jd-selected');
   1767     } else {
   1768         $li.attr('class','jd-autocomplete');
   1769     }
   1770 }
   1771 
   1772 function set_item_values(toroot, $li, match)
   1773 {
   1774     var $link = $('a',$li);
   1775     $link.html(match.__hilabel || match.label);
   1776     $link.attr('href',toroot + match.link);
   1777 }
   1778 
   1779 function set_item_values_jd(toroot, $li, match)
   1780 {
   1781     var $link = $('a',$li);
   1782     $link.html(match.title);
   1783     $link.attr('href',toroot + match.url);
   1784 }
   1785 
   1786 function new_suggestion($list) {
   1787     var $li = $("<li class='jd-autocomplete'></li>");
   1788     $list.append($li);
   1789 
   1790     $li.mousedown(function() {
   1791         window.location = this.firstChild.getAttribute("href");
   1792     });
   1793     $li.mouseover(function() {
   1794         $('.search_filtered_wrapper li').removeClass('jd-selected');
   1795         $(this).addClass('jd-selected');
   1796         gSelectedColumn = $(".search_filtered:visible").index($(this).closest('.search_filtered'));
   1797         gSelectedIndex = $("li", $(".search_filtered:visible")[gSelectedColumn]).index(this);
   1798     });
   1799     $li.append("<a onclick='onSuggestionClick(this)'></a>");
   1800     $li.attr('class','show-item');
   1801     return $li;
   1802 }
   1803 
   1804 function sync_selection_table(toroot)
   1805 {
   1806     var $li; //list item jquery object
   1807     var i; //list item iterator
   1808 
   1809     // if there are NO results at all, hide all columns
   1810     if (!(gMatches.length > 0) && !(gGoogleMatches.length > 0) && !(gDocsMatches.length > 0)) {
   1811         $('.suggest-card').hide(300);
   1812         return;
   1813     }
   1814 
   1815     // if there are api results
   1816     if ((gMatches.length > 0) || (gGoogleMatches.length > 0)) {
   1817       // reveal suggestion list
   1818       $('.suggest-card.reference').show();
   1819       var listIndex = 0; // list index position
   1820 
   1821       // reset the lists
   1822       $(".suggest-card.reference li").remove();
   1823 
   1824       // ########### ANDROID RESULTS #############
   1825       if (gMatches.length > 0) {
   1826 
   1827           // determine android results to show
   1828           gListLength = gMatches.length < ROW_COUNT_FRAMEWORK ?
   1829                         gMatches.length : ROW_COUNT_FRAMEWORK;
   1830           for (i=0; i<gListLength; i++) {
   1831               var $li = new_suggestion($(".suggest-card.reference ul"));
   1832               set_item_values(toroot, $li, gMatches[i]);
   1833               set_item_selected($li, i == gSelectedIndex);
   1834           }
   1835       }
   1836 
   1837       // ########### GOOGLE RESULTS #############
   1838       if (gGoogleMatches.length > 0) {
   1839           // show header for list
   1840           $(".suggest-card.reference ul").append("<li class='header'>in Google Services:</li>");
   1841 
   1842           // determine google results to show
   1843           gGoogleListLength = gGoogleMatches.length < ROW_COUNT_GOOGLE ? gGoogleMatches.length : ROW_COUNT_GOOGLE;
   1844           for (i=0; i<gGoogleListLength; i++) {
   1845               var $li = new_suggestion($(".suggest-card.reference ul"));
   1846               set_item_values(toroot, $li, gGoogleMatches[i]);
   1847               set_item_selected($li, i == gSelectedIndex);
   1848           }
   1849       }
   1850     } else {
   1851       $('.suggest-card.reference').hide();
   1852     }
   1853 
   1854     // ########### JD DOC RESULTS #############
   1855     if (gDocsMatches.length > 0) {
   1856         // reset the lists
   1857         $(".suggest-card:not(.reference) li").remove();
   1858 
   1859         // determine google results to show
   1860         // NOTE: The order of the conditions below for the sugg.type MUST BE SPECIFIC:
   1861         // The order must match the reverse order that each section appears as a card in
   1862         // the suggestion UI... this may be only for the "develop" grouped items though.
   1863         gDocsListLength = gDocsMatches.length < ROW_COUNT_DOCS ? gDocsMatches.length : ROW_COUNT_DOCS;
   1864         for (i=0; i<gDocsListLength; i++) {
   1865             var sugg = gDocsMatches[i];
   1866             var $li;
   1867             if (sugg.type == "design") {
   1868                 $li = new_suggestion($(".suggest-card.design ul"));
   1869             } else
   1870             if (sugg.type == "distribute") {
   1871                 $li = new_suggestion($(".suggest-card.distribute ul"));
   1872             } else
   1873             if (sugg.type == "samples") {
   1874                 $li = new_suggestion($(".suggest-card.develop .child-card.samples"));
   1875             } else
   1876             if (sugg.type == "training") {
   1877                 $li = new_suggestion($(".suggest-card.develop .child-card.training"));
   1878             } else
   1879             if (sugg.type == "about"||"guide"||"tools"||"google") {
   1880                 $li = new_suggestion($(".suggest-card.develop .child-card.guides"));
   1881             } else {
   1882               continue;
   1883             }
   1884 
   1885             set_item_values_jd(toroot, $li, sugg);
   1886             set_item_selected($li, i == gSelectedIndex);
   1887         }
   1888 
   1889         // add heading and show or hide card
   1890         if ($(".suggest-card.design li").length > 0) {
   1891           $(".suggest-card.design ul").prepend("<li class='header'>Design:</li>");
   1892           $(".suggest-card.design").show(300);
   1893         } else {
   1894           $('.suggest-card.design').hide(300);
   1895         }
   1896         if ($(".suggest-card.distribute li").length > 0) {
   1897           $(".suggest-card.distribute ul").prepend("<li class='header'>Distribute:</li>");
   1898           $(".suggest-card.distribute").show(300);
   1899         } else {
   1900           $('.suggest-card.distribute').hide(300);
   1901         }
   1902         if ($(".child-card.guides li").length > 0) {
   1903           $(".child-card.guides").prepend("<li class='header'>Guides:</li>");
   1904           $(".child-card.guides li").appendTo(".suggest-card.develop ul");
   1905         }
   1906         if ($(".child-card.training li").length > 0) {
   1907           $(".child-card.training").prepend("<li class='header'>Training:</li>");
   1908           $(".child-card.training li").appendTo(".suggest-card.develop ul");
   1909         }
   1910         if ($(".child-card.samples li").length > 0) {
   1911           $(".child-card.samples").prepend("<li class='header'>Samples:</li>");
   1912           $(".child-card.samples li").appendTo(".suggest-card.develop ul");
   1913         }
   1914 
   1915         if ($(".suggest-card.develop li").length > 0) {
   1916           $(".suggest-card.develop").show(300);
   1917         } else {
   1918           $('.suggest-card.develop').hide(300);
   1919         }
   1920 
   1921     } else {
   1922       $('.suggest-card:not(.reference)').hide(300);
   1923     }
   1924 }
   1925 
   1926 /** Called by the search input's onkeydown and onkeyup events.
   1927   * Handles navigation with keyboard arrows, Enter key to invoke search,
   1928   * otherwise invokes search suggestions on key-up event.
   1929   * @param e       The JS event
   1930   * @param kd      True if the event is key-down
   1931   * @param toroot  A string for the site's root path
   1932   * @returns       True if the event should bubble up
   1933   */
   1934 function search_changed(e, kd, toroot)
   1935 {
   1936     var currentLang = getLangPref();
   1937     var search = document.getElementById("search_autocomplete");
   1938     var text = search.value.replace(/(^ +)|( +$)/g, '');
   1939     // get the ul hosting the currently selected item
   1940     gSelectedColumn = gSelectedColumn >= 0 ? gSelectedColumn :  0;
   1941     var $columns = $(".search_filtered_wrapper").find(".search_filtered:visible");
   1942     var $selectedUl = $columns[gSelectedColumn];
   1943 
   1944     // show/hide the close button
   1945     if (text != '') {
   1946         $("#search-close").removeClass("hide");
   1947     } else {
   1948         $("#search-close").addClass("hide");
   1949     }
   1950     // 27 = esc
   1951     if (e.keyCode == 27) {
   1952         // close all search results
   1953         if (kd) $('#search-close').trigger('click');
   1954         return true;
   1955     }
   1956     // 13 = enter
   1957     else if (e.keyCode == 13) {
   1958         if (gSelectedIndex < 0) {
   1959             $('.suggest-card').hide();
   1960             if ($("#searchResults").is(":hidden") && (search.value != "")) {
   1961               // if results aren't showing (and text not empty), return true to allow search to execute
   1962               $('body,html').animate({scrollTop:0}, '500', 'swing');
   1963               return true;
   1964             } else {
   1965               // otherwise, results are already showing, so allow ajax to auto refresh the results
   1966               // and ignore this Enter press to avoid the reload.
   1967               return false;
   1968             }
   1969         } else if (kd && gSelectedIndex >= 0) {
   1970             // click the link corresponding to selected item
   1971             $("a",$("li",$selectedUl)[gSelectedIndex]).get()[0].click();
   1972             return false;
   1973         }
   1974     }
   1975     // If Google results are showing, return true to allow ajax search to execute
   1976     else if ($("#searchResults").is(":visible")) {
   1977         // Also, if search_results is scrolled out of view, scroll to top to make results visible
   1978         if ((sticky ) && (search.value != "")) {
   1979           $('body,html').animate({scrollTop:0}, '500', 'swing');
   1980         }
   1981         return true;
   1982     }
   1983     // 38 UP ARROW
   1984     else if (kd && (e.keyCode == 38)) {
   1985         // if the next item is a header, skip it
   1986         if ($($("li", $selectedUl)[gSelectedIndex-1]).hasClass("header")) {
   1987             gSelectedIndex--;
   1988         }
   1989         if (gSelectedIndex >= 0) {
   1990             $('li', $selectedUl).removeClass('jd-selected');
   1991             gSelectedIndex--;
   1992             $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
   1993             // If user reaches top, reset selected column
   1994             if (gSelectedIndex < 0) {
   1995               gSelectedColumn = -1;
   1996             }
   1997         }
   1998         return false;
   1999     }
   2000     // 40 DOWN ARROW
   2001     else if (kd && (e.keyCode == 40)) {
   2002         // if the next item is a header, skip it
   2003         if ($($("li", $selectedUl)[gSelectedIndex+1]).hasClass("header")) {
   2004             gSelectedIndex++;
   2005         }
   2006         if ((gSelectedIndex < $("li", $selectedUl).length-1) ||
   2007                         ($($("li", $selectedUl)[gSelectedIndex+1]).hasClass("header"))) {
   2008             $('li', $selectedUl).removeClass('jd-selected');
   2009             gSelectedIndex++;
   2010             $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
   2011         }
   2012         return false;
   2013     }
   2014     // Consider left/right arrow navigation
   2015     // NOTE: Order of suggest columns are reverse order (index position 0 is on right)
   2016     else if (kd && $columns.length > 1 && gSelectedColumn >= 0) {
   2017       // 37 LEFT ARROW
   2018       // go left only if current column is not left-most column (last column)
   2019       if (e.keyCode == 37 && gSelectedColumn < $columns.length - 1) {
   2020         $('li', $selectedUl).removeClass('jd-selected');
   2021         gSelectedColumn++;
   2022         $selectedUl = $columns[gSelectedColumn];
   2023         // keep or reset the selected item to last item as appropriate
   2024         gSelectedIndex = gSelectedIndex >
   2025                 $("li", $selectedUl).length-1 ?
   2026                 $("li", $selectedUl).length-1 : gSelectedIndex;
   2027         // if the corresponding item is a header, move down
   2028         if ($($("li", $selectedUl)[gSelectedIndex]).hasClass("header")) {
   2029           gSelectedIndex++;
   2030         }
   2031         // set item selected
   2032         $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
   2033         return false;
   2034       }
   2035       // 39 RIGHT ARROW
   2036       // go right only if current column is not the right-most column (first column)
   2037       else if (e.keyCode == 39 && gSelectedColumn > 0) {
   2038         $('li', $selectedUl).removeClass('jd-selected');
   2039         gSelectedColumn--;
   2040         $selectedUl = $columns[gSelectedColumn];
   2041         // keep or reset the selected item to last item as appropriate
   2042         gSelectedIndex = gSelectedIndex >
   2043                 $("li", $selectedUl).length-1 ?
   2044                 $("li", $selectedUl).length-1 : gSelectedIndex;
   2045         // if the corresponding item is a header, move down
   2046         if ($($("li", $selectedUl)[gSelectedIndex]).hasClass("header")) {
   2047           gSelectedIndex++;
   2048         }
   2049         // set item selected
   2050         $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
   2051         return false;
   2052       }
   2053     }
   2054 
   2055     // if key-up event and not arrow down/up/left/right,
   2056     // read the search query and add suggestions to gMatches
   2057     else if (!kd && (e.keyCode != 40)
   2058                  && (e.keyCode != 38)
   2059                  && (e.keyCode != 37)
   2060                  && (e.keyCode != 39)) {
   2061         gSelectedIndex = -1;
   2062         gMatches = new Array();
   2063         matchedCount = 0;
   2064         gGoogleMatches = new Array();
   2065         matchedCountGoogle = 0;
   2066         gDocsMatches = new Array();
   2067         matchedCountDocs = 0;
   2068 
   2069         // Search for Android matches
   2070         for (var i=0; i<DATA.length; i++) {
   2071             var s = DATA[i];
   2072             if (text.length != 0 &&
   2073                   s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) {
   2074                 gMatches[matchedCount] = s;
   2075                 matchedCount++;
   2076             }
   2077         }
   2078         rank_autocomplete_api_results(text, gMatches);
   2079         for (var i=0; i<gMatches.length; i++) {
   2080             var s = gMatches[i];
   2081         }
   2082 
   2083 
   2084         // Search for Google matches
   2085         for (var i=0; i<GOOGLE_DATA.length; i++) {
   2086             var s = GOOGLE_DATA[i];
   2087             if (text.length != 0 &&
   2088                   s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) {
   2089                 gGoogleMatches[matchedCountGoogle] = s;
   2090                 matchedCountGoogle++;
   2091             }
   2092         }
   2093         rank_autocomplete_api_results(text, gGoogleMatches);
   2094         for (var i=0; i<gGoogleMatches.length; i++) {
   2095             var s = gGoogleMatches[i];
   2096         }
   2097 
   2098         highlight_autocomplete_result_labels(text);
   2099 
   2100 
   2101 
   2102         // Search for matching JD docs
   2103         if (text.length >= 2) {
   2104           // match only the beginning of a word
   2105           var queryStr = text.toLowerCase();
   2106 
   2107           // Search for Training classes
   2108           for (var i=0; i<TRAINING_RESOURCES.length; i++) {
   2109             // current search comparison, with counters for tag and title,
   2110             // used later to improve ranking
   2111             var s = TRAINING_RESOURCES[i];
   2112             s.matched_tag = 0;
   2113             s.matched_title = 0;
   2114             var matched = false;
   2115 
   2116             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2117             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2118               // it matches a tag
   2119               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2120                 matched = true;
   2121                 s.matched_tag = j + 1; // add 1 to index position
   2122               }
   2123             }
   2124             // Don't consider doc title for lessons (only for class landing pages),
   2125             // unless the lesson has a tag that already matches
   2126             if ((s.lang == currentLang) &&
   2127                   (!(s.type == "training" && s.url.indexOf("index.html") == -1) || matched)) {
   2128               // it matches the doc title
   2129               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2130                 matched = true;
   2131                 s.matched_title = 1;
   2132               }
   2133             }
   2134             if (matched) {
   2135               gDocsMatches[matchedCountDocs] = s;
   2136               matchedCountDocs++;
   2137             }
   2138           }
   2139 
   2140 
   2141           // Search for API Guides
   2142           for (var i=0; i<GUIDE_RESOURCES.length; i++) {
   2143             // current search comparison, with counters for tag and title,
   2144             // used later to improve ranking
   2145             var s = GUIDE_RESOURCES[i];
   2146             s.matched_tag = 0;
   2147             s.matched_title = 0;
   2148             var matched = false;
   2149 
   2150             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2151             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2152               // it matches a tag
   2153               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2154 
   2155                 matched = true;
   2156                 s.matched_tag = j + 1; // add 1 to index position
   2157               }
   2158             }
   2159             // Check if query matches the doc title, but only for current language
   2160             if (s.lang == currentLang) {
   2161               // if query matches the doc title
   2162               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2163                 matched = true;
   2164                 s.matched_title = 1;
   2165               }
   2166             }
   2167             if (matched) {
   2168               gDocsMatches[matchedCountDocs] = s;
   2169               matchedCountDocs++;
   2170             }
   2171           }
   2172 
   2173 
   2174           // Search for Tools Guides
   2175           for (var i=0; i<TOOLS_RESOURCES.length; i++) {
   2176             // current search comparison, with counters for tag and title,
   2177             // used later to improve ranking
   2178             var s = TOOLS_RESOURCES[i];
   2179             s.matched_tag = 0;
   2180             s.matched_title = 0;
   2181             var matched = false;
   2182 
   2183             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2184             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2185               // it matches a tag
   2186                 if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2187                 matched = true;
   2188                 s.matched_tag = j + 1; // add 1 to index position
   2189               }
   2190             }
   2191             // Check if query matches the doc title, but only for current language
   2192             if (s.lang == currentLang) {
   2193               // if query matches the doc title
   2194                 if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2195                 matched = true;
   2196                 s.matched_title = 1;
   2197               }
   2198             }
   2199             if (matched) {
   2200               gDocsMatches[matchedCountDocs] = s;
   2201               matchedCountDocs++;
   2202             }
   2203           }
   2204 
   2205 
   2206           // Search for About docs
   2207           for (var i=0; i<ABOUT_RESOURCES.length; i++) {
   2208             // current search comparison, with counters for tag and title,
   2209             // used later to improve ranking
   2210             var s = ABOUT_RESOURCES[i];
   2211             s.matched_tag = 0;
   2212             s.matched_title = 0;
   2213             var matched = false;
   2214 
   2215             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2216             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2217               // it matches a tag
   2218               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2219                 matched = true;
   2220                 s.matched_tag = j + 1; // add 1 to index position
   2221               }
   2222             }
   2223             // Check if query matches the doc title, but only for current language
   2224             if (s.lang == currentLang) {
   2225               // if query matches the doc title
   2226               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2227                 matched = true;
   2228                 s.matched_title = 1;
   2229               }
   2230             }
   2231             if (matched) {
   2232               gDocsMatches[matchedCountDocs] = s;
   2233               matchedCountDocs++;
   2234             }
   2235           }
   2236 
   2237 
   2238           // Search for Design guides
   2239           for (var i=0; i<DESIGN_RESOURCES.length; i++) {
   2240             // current search comparison, with counters for tag and title,
   2241             // used later to improve ranking
   2242             var s = DESIGN_RESOURCES[i];
   2243             s.matched_tag = 0;
   2244             s.matched_title = 0;
   2245             var matched = false;
   2246 
   2247             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2248             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2249               // it matches a tag
   2250               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2251                 matched = true;
   2252                 s.matched_tag = j + 1; // add 1 to index position
   2253               }
   2254             }
   2255             // Check if query matches the doc title, but only for current language
   2256             if (s.lang == currentLang) {
   2257               // if query matches the doc title
   2258               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2259                 matched = true;
   2260                 s.matched_title = 1;
   2261               }
   2262             }
   2263             if (matched) {
   2264               gDocsMatches[matchedCountDocs] = s;
   2265               matchedCountDocs++;
   2266             }
   2267           }
   2268 
   2269 
   2270           // Search for Distribute guides
   2271           for (var i=0; i<DISTRIBUTE_RESOURCES.length; i++) {
   2272             // current search comparison, with counters for tag and title,
   2273             // used later to improve ranking
   2274             var s = DISTRIBUTE_RESOURCES[i];
   2275             s.matched_tag = 0;
   2276             s.matched_title = 0;
   2277             var matched = false;
   2278 
   2279             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2280             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2281               // it matches a tag
   2282               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2283                 matched = true;
   2284                 s.matched_tag = j + 1; // add 1 to index position
   2285               }
   2286             }
   2287             // Check if query matches the doc title, but only for current language
   2288             if (s.lang == currentLang) {
   2289               // if query matches the doc title
   2290               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2291                 matched = true;
   2292                 s.matched_title = 1;
   2293               }
   2294             }
   2295             if (matched) {
   2296               gDocsMatches[matchedCountDocs] = s;
   2297               matchedCountDocs++;
   2298             }
   2299           }
   2300 
   2301 
   2302           // Search for Google guides
   2303           for (var i=0; i<GOOGLE_RESOURCES.length; i++) {
   2304             // current search comparison, with counters for tag and title,
   2305             // used later to improve ranking
   2306             var s = GOOGLE_RESOURCES[i];
   2307             s.matched_tag = 0;
   2308             s.matched_title = 0;
   2309             var matched = false;
   2310 
   2311             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2312             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2313               // it matches a tag
   2314               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2315                 matched = true;
   2316                 s.matched_tag = j + 1; // add 1 to index position
   2317               }
   2318             }
   2319             // Check if query matches the doc title, but only for current language
   2320             if (s.lang == currentLang) {
   2321               // if query matches the doc title
   2322               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2323                 matched = true;
   2324                 s.matched_title = 1;
   2325               }
   2326             }
   2327             if (matched) {
   2328               gDocsMatches[matchedCountDocs] = s;
   2329               matchedCountDocs++;
   2330             }
   2331           }
   2332 
   2333 
   2334           // Search for Samples
   2335           for (var i=0; i<SAMPLES_RESOURCES.length; i++) {
   2336             // current search comparison, with counters for tag and title,
   2337             // used later to improve ranking
   2338             var s = SAMPLES_RESOURCES[i];
   2339             s.matched_tag = 0;
   2340             s.matched_title = 0;
   2341             var matched = false;
   2342             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2343             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2344               // it matches a tag
   2345               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2346                 matched = true;
   2347                 s.matched_tag = j + 1; // add 1 to index position
   2348               }
   2349             }
   2350             // Check if query matches the doc title, but only for current language
   2351             if (s.lang == currentLang) {
   2352               // if query matches the doc title.t
   2353               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2354                 matched = true;
   2355                 s.matched_title = 1;
   2356               }
   2357             }
   2358             if (matched) {
   2359               gDocsMatches[matchedCountDocs] = s;
   2360               matchedCountDocs++;
   2361             }
   2362           }
   2363 
   2364           // Search for Preview Guides
   2365           for (var i=0; i<PREVIEW_RESOURCES.length; i++) {
   2366             // current search comparison, with counters for tag and title,
   2367             // used later to improve ranking
   2368             var s = PREVIEW_RESOURCES[i];
   2369             s.matched_tag = 0;
   2370             s.matched_title = 0;
   2371             var matched = false;
   2372 
   2373             // Check if query matches any tags; work backwards toward 1 to assist ranking
   2374             for (var j = s.keywords.length - 1; j >= 0; j--) {
   2375               // it matches a tag
   2376               if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
   2377                 matched = true;
   2378                 s.matched_tag = j + 1; // add 1 to index position
   2379               }
   2380             }
   2381             // Check if query matches the doc title, but only for current language
   2382             if (s.lang == currentLang) {
   2383               // if query matches the doc title
   2384               if (s.title.toLowerCase().indexOf(queryStr) == 0) {
   2385                 matched = true;
   2386                 s.matched_title = 1;
   2387               }
   2388             }
   2389             if (matched) {
   2390               gDocsMatches[matchedCountDocs] = s;
   2391               matchedCountDocs++;
   2392             }
   2393           }
   2394 
   2395           // Rank/sort all the matched pages
   2396           rank_autocomplete_doc_results(text, gDocsMatches);
   2397         }
   2398 
   2399         // draw the suggestions
   2400         sync_selection_table(toroot);
   2401         return true; // allow the event to bubble up to the search api
   2402     }
   2403 }
   2404 
   2405 /* Order the jd doc result list based on match quality */
   2406 function rank_autocomplete_doc_results(query, matches) {
   2407     query = query || '';
   2408     if (!matches || !matches.length)
   2409       return;
   2410 
   2411     var _resultScoreFn = function(match) {
   2412         var score = 1.0;
   2413 
   2414         // if the query matched a tag
   2415         if (match.matched_tag > 0) {
   2416           // multiply score by factor relative to position in tags list (max of 3)
   2417           score *= 3 / match.matched_tag;
   2418 
   2419           // if it also matched the title
   2420           if (match.matched_title > 0) {
   2421             score *= 2;
   2422           }
   2423         } else if (match.matched_title > 0) {
   2424           score *= 3;
   2425         }
   2426 
   2427         return score;
   2428     };
   2429 
   2430     for (var i=0; i<matches.length; i++) {
   2431         matches[i].__resultScore = _resultScoreFn(matches[i]);
   2432     }
   2433 
   2434     matches.sort(function(a,b){
   2435         var n = b.__resultScore - a.__resultScore;
   2436         if (n == 0) // lexicographical sort if scores are the same
   2437             n = (a.label < b.label) ? -1 : 1;
   2438         return n;
   2439     });
   2440 }
   2441 
   2442 /* Order the result list based on match quality */
   2443 function rank_autocomplete_api_results(query, matches) {
   2444     query = query || '';
   2445     if (!matches || !matches.length)
   2446       return;
   2447 
   2448     // helper function that gets the last occurence index of the given regex
   2449     // in the given string, or -1 if not found
   2450     var _lastSearch = function(s, re) {
   2451       if (s == '')
   2452         return -1;
   2453       var l = -1;
   2454       var tmp;
   2455       while ((tmp = s.search(re)) >= 0) {
   2456         if (l < 0) l = 0;
   2457         l += tmp;
   2458         s = s.substr(tmp + 1);
   2459       }
   2460       return l;
   2461     };
   2462 
   2463     // helper function that counts the occurrences of a given character in
   2464     // a given string
   2465     var _countChar = function(s, c) {
   2466       var n = 0;
   2467       for (var i=0; i<s.length; i++)
   2468         if (s.charAt(i) == c) ++n;
   2469       return n;
   2470     };
   2471 
   2472     var queryLower = query.toLowerCase();
   2473     var queryAlnum = (queryLower.match(/\w+/) || [''])[0];
   2474     var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum);
   2475     var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b');
   2476 
   2477     var _resultScoreFn = function(result) {
   2478         // scores are calculated based on exact and prefix matches,
   2479         // and then number of path separators (dots) from the last
   2480         // match (i.e. favoring classes and deep package names)
   2481         var score = 1.0;
   2482         var labelLower = result.label.toLowerCase();
   2483         var t;
   2484         t = _lastSearch(labelLower, partExactAlnumRE);
   2485         if (t >= 0) {
   2486             // exact part match
   2487             var partsAfter = _countChar(labelLower.substr(t + 1), '.');
   2488             score *= 200 / (partsAfter + 1);
   2489         } else {
   2490             t = _lastSearch(labelLower, partPrefixAlnumRE);
   2491             if (t >= 0) {
   2492                 // part prefix match
   2493                 var partsAfter = _countChar(labelLower.substr(t + 1), '.');
   2494                 score *= 20 / (partsAfter + 1);
   2495             }
   2496         }
   2497 
   2498         return score;
   2499     };
   2500 
   2501     for (var i=0; i<matches.length; i++) {
   2502         // if the API is deprecated, default score is 0; otherwise, perform scoring
   2503         if (matches[i].deprecated == "true") {
   2504           matches[i].__resultScore = 0;
   2505         } else {
   2506           matches[i].__resultScore = _resultScoreFn(matches[i]);
   2507         }
   2508     }
   2509 
   2510     matches.sort(function(a,b){
   2511         var n = b.__resultScore - a.__resultScore;
   2512         if (n == 0) // lexicographical sort if scores are the same
   2513             n = (a.label < b.label) ? -1 : 1;
   2514         return n;
   2515     });
   2516 }
   2517 
   2518 /* Add emphasis to part of string that matches query */
   2519 function highlight_autocomplete_result_labels(query) {
   2520     query = query || '';
   2521     if ((!gMatches || !gMatches.length) && (!gGoogleMatches || !gGoogleMatches.length))
   2522       return;
   2523 
   2524     var queryLower = query.toLowerCase();
   2525     var queryAlnumDot = (queryLower.match(/[\w\.]+/) || [''])[0];
   2526     var queryRE = new RegExp(
   2527         '(' + queryAlnumDot.replace(/\./g, '\\.') + ')', 'ig');
   2528     for (var i=0; i<gMatches.length; i++) {
   2529         gMatches[i].__hilabel = gMatches[i].label.replace(
   2530             queryRE, '<b>$1</b>');
   2531     }
   2532     for (var i=0; i<gGoogleMatches.length; i++) {
   2533         gGoogleMatches[i].__hilabel = gGoogleMatches[i].label.replace(
   2534             queryRE, '<b>$1</b>');
   2535     }
   2536 }
   2537 
   2538 function search_focus_changed(obj, focused)
   2539 {
   2540     if (!focused) {
   2541         if(obj.value == ""){
   2542           $("#search-close").addClass("hide");
   2543         }
   2544         $(".suggest-card").hide();
   2545     }
   2546 }
   2547 
   2548 function submit_search() {
   2549   var query = escapeHTML(document.getElementById('search_autocomplete').value);
   2550   location.hash = 'q=' + query;
   2551   searchControl.query = query;
   2552   searchControl.init();
   2553   searchControl.trackSearchRequest(query);
   2554   $("#searchResults").slideDown('slow', setStickyTop);
   2555   return false;
   2556 }
   2557 
   2558 function hideResults() {
   2559   $("#searchResults").slideUp('fast', setStickyTop);
   2560   $("#search-close").addClass("hide");
   2561   location.hash = '';
   2562 
   2563   $("#search_autocomplete").val("").blur();
   2564 
   2565   // reset the ajax search callback to nothing, so results don't appear unless ENTER
   2566   searchControl.reset();
   2567 
   2568   return false;
   2569 }
   2570 
   2571 /* ########################################################## */
   2572 /* ################  CUSTOM SEARCH ENGINE  ################## */
   2573 /* ########################################################## */
   2574 var searchControl = null;
   2575 var dacsearch = dacsearch || {};
   2576 
   2577 /**
   2578  * The custom search engine API.
   2579  * @constructor
   2580  */
   2581 dacsearch.CustomSearchEngine = function() {
   2582   /**
   2583    * The last response from Google CSE.
   2584    * @private {Object}
   2585    */
   2586   this.resultQuery_ = {};
   2587 
   2588   /** @private {?Element} */
   2589   this.searchResultEl_ = null;
   2590 
   2591   /** @private {?Element} */
   2592   this.searchInputEl_ = null;
   2593 
   2594   /** @private {string} */
   2595   this.query = '';
   2596 };
   2597 
   2598 /**
   2599  * Initializes DAC's Google custom search engine.
   2600  * @export
   2601  */
   2602 dacsearch.CustomSearchEngine.prototype.init = function() {
   2603   this.searchResultEl_ = $('#leftSearchControl');
   2604   this.searchResultEl_.empty();
   2605   this.searchInputEl_ = $('#search_autocomplete');
   2606   this.searchInputEl_.focus().val(this.query);
   2607   this.getResults_();
   2608   this.bindEvents_();
   2609 };
   2610 
   2611 
   2612 /**
   2613  * Binds the keyup event to the search input.
   2614  * @private
   2615  */
   2616 dacsearch.CustomSearchEngine.prototype.bindEvents_ = function() {
   2617   this.searchInputEl_.keyup(this.debounce_(function(e) {
   2618     var code = e.which;
   2619     if (code != 13) {
   2620       this.query = escapeHTML(this.searchInputEl_.val());
   2621       location.hash = 'q=' + encodeURI(this.query);
   2622       this.searchResultEl_.empty();
   2623       this.getResults_();
   2624     }
   2625   }.bind(this), 250));
   2626 };
   2627 
   2628 
   2629 /**
   2630  * Resets the search control.
   2631  */
   2632 dacsearch.CustomSearchEngine.prototype.reset = function() {
   2633   this.query = '';
   2634   this.searchInputEl_.off('keyup');
   2635   this.searchResultEl_.empty();
   2636   this.updateResultTitle_();
   2637 };
   2638 
   2639 
   2640 /**
   2641  * Updates the search query text at the top of the results.
   2642  * @private
   2643  */
   2644 dacsearch.CustomSearchEngine.prototype.updateResultTitle_ = function() {
   2645   $("#searchTitle").html("Results for <em>" + this.query + "</em>");
   2646 };
   2647 
   2648 
   2649 /**
   2650  * Makes the CSE api call and gets the results.
   2651  * @param {number=} opt_start The optional start index.
   2652  * @private
   2653  */
   2654 dacsearch.CustomSearchEngine.prototype.getResults_ = function(opt_start) {
   2655   var lang = getLangPref();
   2656   // Fix zh-cn to be zh-CN.
   2657   lang = lang.replace(/-\w+/, function(m) { return m.toUpperCase(); });
   2658   var cseUrl = 'https://content.googleapis.com/customsearch/v1?';
   2659   var searchParams = {
   2660     cx: '000521750095050289010:zpcpi1ea4s8',
   2661     key: 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8',
   2662     q: this.query,
   2663     start: opt_start || 1,
   2664     num: 6,
   2665     hl: lang,
   2666     fields: 'queries,items(pagemap,link,title,htmlSnippet,formattedUrl)'
   2667   };
   2668 
   2669   $.get(cseUrl + $.param(searchParams), function(data) {
   2670     this.resultQuery_ = data;
   2671     this.renderResults_(data);
   2672     this.updateResultTitle_(this.query);
   2673   }.bind(this));
   2674 };
   2675 
   2676 
   2677 /**
   2678  * Renders the results.
   2679  * @private
   2680  */
   2681 dacsearch.CustomSearchEngine.prototype.renderResults_ = function(results) {
   2682   var el = this.searchResultEl_;
   2683 
   2684   if (!results.items) {
   2685     el.append($('<div>').text('No results'));
   2686     return;
   2687   }
   2688 
   2689   for (var i = 0; i < results.items.length; i++) {
   2690     var item = results.items[i];
   2691     var hasImage = item.pagemap && item.pagemap.cse_thumbnail;
   2692     var sectionMatch = item.link.match(/developer\.android\.com\/(\w*)/);
   2693     var section = (sectionMatch && sectionMatch[1]) || 'blog';
   2694 
   2695     var entry = $('<div>').addClass('dac-custom-search-entry cols');
   2696 
   2697     if (hasImage) {
   2698       var image = item.pagemap.cse_thumbnail[0];
   2699       entry.append($('<div>').addClass('col-1of6')
   2700         .append($('<div>').addClass('dac-custom-search-image').css(
   2701         'background-image', 'url(' + image.src + ')')));
   2702     }
   2703 
   2704     var linkTitleEl = $('<a>').text(item.title).attr('href', item.link);
   2705     linkTitleEl.click(function(e) {
   2706       ga('send', 'event', 'Google Custom Search',
   2707           'clicked: ' + linkTitleEl.attr('href'),
   2708           'query: ' + $("#search_autocomplete").val().toLowerCase());
   2709     });
   2710 
   2711     var linkUrlEl = $('<a>').addClass('dac-custom-search-link').text(
   2712         item.formattedUrl).attr('href', item.link);
   2713     linkUrlEl.click(function(e) {
   2714       ga('send', 'event', 'Google Custom Search',
   2715           'clicked: ' + linkUrlEl.attr('href'),
   2716           'query: ' + $("#search_autocomplete").val().toLowerCase());
   2717     });
   2718 
   2719 
   2720     entry.append($('<div>').addClass(hasImage ? 'col-5of6' : 'col-6of6')
   2721       .append($('<p>').addClass('dac-custom-search-section').text(section))
   2722       .append(
   2723         linkTitleEl.wrap('<h2>').parent().addClass('dac-custom-search-title'))
   2724       .append($('<p>').addClass('dac-custom-search-snippet')
   2725       .html(item.htmlSnippet.replace(/<br>/g, ''))).append(linkUrlEl));
   2726 
   2727     el.append(entry);
   2728   }
   2729 
   2730   if ($('#dac-custom-search-load-more')) {
   2731     $('#dac-custom-search-load-more').remove();
   2732   }
   2733 
   2734   if (results.queries.nextPage) {
   2735     var loadMoreButton = $('<button id="dac-custom-search-load-more">')
   2736       .addClass('dac-custom-search-load-more')
   2737       .text('Load more')
   2738       .click(function() {
   2739         this.loadMoreResults_();
   2740       }.bind(this));
   2741 
   2742     el.append(loadMoreButton);
   2743   }
   2744 };
   2745 
   2746 
   2747 /**
   2748  * Loads more results.
   2749  * @private
   2750  */
   2751 dacsearch.CustomSearchEngine.prototype.loadMoreResults_ = function() {
   2752   this.query = this.resultQuery_.queries.request[0].searchTerms;
   2753   var start = this.resultQuery_.queries.nextPage[0].startIndex;
   2754   var loadMoreButton = this.searchResultEl_.find(
   2755       '#dac-custom-search-load-more');
   2756   loadMoreButton.text('Loading more...');
   2757   this.getResults_(start);
   2758   this.trackSearchRequest(this.query + ' startIndex = ' + start);
   2759 };
   2760 
   2761 
   2762 /**
   2763  * Tracks a search request.
   2764  * @param {string} query The query for the request,
   2765  *                       includes start index if loading more results.
   2766  */
   2767 dacsearch.CustomSearchEngine.prototype.trackSearchRequest = function(query) {
   2768   ga('send', 'event', 'Google Custom Search Submit', 'submit search query',
   2769       'query: ' + query);
   2770 };
   2771 
   2772 
   2773 /**
   2774  * Returns a function, that, as long as it continues to be invoked, will not
   2775  * be triggered. The function will be called after it stops being called for
   2776  * N milliseconds.
   2777  * @param {Function} func The function to debounce.
   2778  * @param {number} wait The number of milliseconds to wait before calling the function.
   2779  * @private
   2780  */
   2781 dacsearch.CustomSearchEngine.prototype.debounce_ = function(func, wait) {
   2782   var timeout;
   2783   return function() {
   2784     var context = this, args = arguments;
   2785     var later = function() {
   2786       timeout = null;
   2787       func.apply(context, args);
   2788     };
   2789    clearTimeout(timeout);
   2790    timeout = setTimeout(later, wait);
   2791   };
   2792 };
   2793 
   2794 
   2795 google.setOnLoadCallback(function(){
   2796   searchControl = new dacsearch.CustomSearchEngine();
   2797   if (location.hash.indexOf("q=") == -1) {
   2798     // if there's no query in the url, don't search and make sure results are hidden
   2799     $('#searchResults').hide();
   2800     return;
   2801   } else {
   2802     // first time loading search results for this page
   2803     searchControl.query = escapeHTML(decodeURI(location.hash.split('q=')[1]));
   2804     searchControl.init();
   2805     searchControl.trackSearchRequest(searchControl.query);
   2806     $('#searchResults').slideDown('slow', setStickyTop);
   2807     $("#search-close").removeClass("hide");
   2808   }
   2809 }, true);
   2810 
   2811 /* Adjust the scroll position to account for sticky header, only if the hash matches an id.
   2812    This does not handle <a name=""> tags. Some CSS fixes those, but only for reference docs. */
   2813 function offsetScrollForSticky() {
   2814   // Ignore if there's no search bar (some special pages have no header)
   2815   if ($("#search-container").length < 1) return;
   2816 
   2817   var hash = escape(location.hash.substr(1));
   2818   var $matchingElement = $("#"+hash);
   2819   // Sanity check that there's an element with that ID on the page
   2820   if ($matchingElement.length) {
   2821     // If the position of the target element is near the top of the page (<20px, where we expect it
   2822     // to be because we need to move it down 60px to become in view), then move it down 60px
   2823     if (Math.abs($matchingElement.offset().top - $(window).scrollTop()) < 20) {
   2824       $(window).scrollTop($(window).scrollTop() - 60);
   2825     }
   2826   }
   2827 }
   2828 
   2829 // when an event on the browser history occurs (back, forward, load) requery hash and do search
   2830 $(window).hashchange( function(){
   2831   // Ignore if there's no search bar (some special pages have no header)
   2832   if ($("#search-container").length < 1) return;
   2833 
   2834   // If the hash isn't a search query or there's an error in the query,
   2835   // then adjust the scroll position to account for sticky header, then exit.
   2836   if ((location.hash.indexOf("q=") == -1) || (searchControl.query == "undefined")) {
   2837     // If the results pane is open, close it.
   2838     if (!$("#searchResults").is(":hidden")) {
   2839       hideResults();
   2840     }
   2841     offsetScrollForSticky();
   2842     return;
   2843   }
   2844 
   2845   $('#searchResults').slideDown('slow', setStickyTop);
   2846   $("#search_autocomplete").focus();
   2847   $("#search-close").removeClass("hide");
   2848 });
   2849 
   2850 /* returns the given string with all HTML brackets converted to entities
   2851     TODO: move this to the site's JS library */
   2852 function escapeHTML(string) {
   2853   return string.replace(/</g,"&lt;")
   2854                 .replace(/>/g,"&gt;");
   2855 }
   2856 
   2857 
   2858 
   2859 
   2860 
   2861 
   2862 
   2863 /* ######################################################## */
   2864 /* #################  JAVADOC REFERENCE ################### */
   2865 /* ######################################################## */
   2866 
   2867 /* Initialize some droiddoc stuff, but only if we're in the reference */
   2868 if (location.pathname.indexOf("/reference") == 0) {
   2869   if(!(location.pathname.indexOf("/reference-gms/packages.html") == 0)
   2870     && !(location.pathname.indexOf("/reference-gcm/packages.html") == 0)
   2871     && !(location.pathname.indexOf("/reference/com/google") == 0)) {
   2872     $(document).ready(function() {
   2873       // init available apis based on user pref
   2874       changeApiLevel();
   2875       initSidenavHeightResize()
   2876       });
   2877   }
   2878 }
   2879 
   2880 var API_LEVEL_COOKIE = "api_level";
   2881 var minLevel = 1;
   2882 var maxLevel = 1;
   2883 
   2884 /******* SIDENAV DIMENSIONS ************/
   2885 
   2886   function initSidenavHeightResize() {
   2887     // Change the drag bar size to nicely fit the scrollbar positions
   2888     var $dragBar = $(".ui-resizable-s");
   2889     $dragBar.css({'width': $dragBar.parent().width() - 5 + "px"});
   2890 
   2891     $( "#resize-packages-nav" ).resizable({
   2892       containment: "#nav-panels",
   2893       handles: "s",
   2894       alsoResize: "#packages-nav",
   2895       resize: function(event, ui) { resizeNav(); }, /* resize the nav while dragging */
   2896       stop: function(event, ui) { saveNavPanels(); } /* once stopped, save the sizes to cookie  */
   2897       });
   2898 
   2899   }
   2900 
   2901 function updateSidenavFixedWidth() {
   2902   if (!sticky) return;
   2903   $('#devdoc-nav').css({
   2904     'width' : $('#side-nav').css('width'),
   2905     'margin' : $('#side-nav').css('margin')
   2906   });
   2907   $('#devdoc-nav a.totop').css({'display':'block','width':$("#nav").innerWidth()+'px'});
   2908 
   2909   initSidenavHeightResize();
   2910 }
   2911 
   2912 function updateSidenavFullscreenWidth() {
   2913   if (!sticky) return;
   2914   $('#devdoc-nav').css({
   2915     'width' : $('#side-nav').css('width'),
   2916     'margin' : $('#side-nav').css('margin')
   2917   });
   2918   $('#devdoc-nav .totop').css({'left': 'inherit'});
   2919 
   2920   initSidenavHeightResize();
   2921 }
   2922 
   2923 function buildApiLevelSelector() {
   2924   maxLevel = SINCE_DATA.length;
   2925   var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE));
   2926   userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default
   2927 
   2928   minLevel = parseInt($("#doc-api-level").attr("class"));
   2929   // Handle provisional api levels; the provisional level will always be the highest possible level
   2930   // Provisional api levels will also have a length; other stuff that's just missing a level won't,
   2931   // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class)
   2932   if (isNaN(minLevel) && minLevel.length) {
   2933     minLevel = maxLevel;
   2934   }
   2935   var select = $("#apiLevelSelector").html("").change(changeApiLevel);
   2936   for (var i = maxLevel-1; i >= 0; i--) {
   2937     var option = $("<option />").attr("value",""+SINCE_DATA[i]).append(""+SINCE_DATA[i]);
   2938   //  if (SINCE_DATA[i] < minLevel) option.addClass("absent"); // always false for strings (codenames)
   2939     select.append(option);
   2940   }
   2941 
   2942   // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true)
   2943   var selectedLevelItem = $("#apiLevelSelector option[value='"+userApiLevel+"']").get(0);
   2944   selectedLevelItem.setAttribute('selected',true);
   2945 }
   2946 
   2947 function changeApiLevel() {
   2948   maxLevel = SINCE_DATA.length;
   2949   var selectedLevel = maxLevel;
   2950 
   2951   selectedLevel = parseInt($("#apiLevelSelector option:selected").val());
   2952   toggleVisisbleApis(selectedLevel, "body");
   2953 
   2954   writeCookie(API_LEVEL_COOKIE, selectedLevel, null);
   2955 
   2956   if (selectedLevel < minLevel) {
   2957     var thing = ($("#jd-header").html().indexOf("package") != -1) ? "package" : "class";
   2958     $("#naMessage").show().html("<div><p><strong>This " + thing
   2959               + " requires API level " + minLevel + " or higher.</strong></p>"
   2960               + "<p>This document is hidden because your selected API level for the documentation is "
   2961               + selectedLevel + ". You can change the documentation API level with the selector "
   2962               + "above the left navigation.</p>"
   2963               + "<p>For more information about specifying the API level your app requires, "
   2964               + "read <a href='" + toRoot + "training/basics/supporting-devices/platforms.html'"
   2965               + ">Supporting Different Platform Versions</a>.</p>"
   2966               + "<input type='button' value='OK, make this page visible' "
   2967               + "title='Change the API level to " + minLevel + "' "
   2968               + "onclick='$(\"#apiLevelSelector\").val(\"" + minLevel + "\");changeApiLevel();' />"
   2969               + "</div>");
   2970   } else {
   2971     $("#naMessage").hide();
   2972   }
   2973 }
   2974 
   2975 function toggleVisisbleApis(selectedLevel, context) {
   2976   var apis = $(".api",context);
   2977   apis.each(function(i) {
   2978     var obj = $(this);
   2979     var className = obj.attr("class");
   2980     var apiLevelIndex = className.lastIndexOf("-")+1;
   2981     var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex);
   2982     apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length;
   2983     var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex);
   2984     if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail
   2985       return;
   2986     }
   2987     apiLevel = parseInt(apiLevel);
   2988 
   2989     // Handle provisional api levels; if this item's level is the provisional one, set it to the max
   2990     var selectedLevelNum = parseInt(selectedLevel)
   2991     var apiLevelNum = parseInt(apiLevel);
   2992     if (isNaN(apiLevelNum)) {
   2993         apiLevelNum = maxLevel;
   2994     }
   2995 
   2996     // Grey things out that aren't available and give a tooltip title
   2997     if (apiLevelNum > selectedLevelNum) {
   2998       obj.addClass("absent").attr("title","Requires API Level \""
   2999             + apiLevel + "\" or higher. To reveal, change the target API level "
   3000               + "above the left navigation.");
   3001     }
   3002     else obj.removeClass("absent").removeAttr("title");
   3003   });
   3004 }
   3005 
   3006 
   3007 
   3008 
   3009 /* #################  SIDENAV TREE VIEW ################### */
   3010 
   3011 function new_node(me, mom, text, link, children_data, api_level)
   3012 {
   3013   var node = new Object();
   3014   node.children = Array();
   3015   node.children_data = children_data;
   3016   node.depth = mom.depth + 1;
   3017 
   3018   node.li = document.createElement("li");
   3019   mom.get_children_ul().appendChild(node.li);
   3020 
   3021   node.label_div = document.createElement("div");
   3022   node.label_div.className = "label";
   3023   if (api_level != null) {
   3024     $(node.label_div).addClass("api");
   3025     $(node.label_div).addClass("api-level-"+api_level);
   3026   }
   3027   node.li.appendChild(node.label_div);
   3028 
   3029   if (children_data != null) {
   3030     node.expand_toggle = document.createElement("a");
   3031     node.expand_toggle.href = "javascript:void(0)";
   3032     node.expand_toggle.onclick = function() {
   3033           if (node.expanded) {
   3034             $(node.get_children_ul()).slideUp("fast");
   3035             node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png";
   3036             node.expanded = false;
   3037           } else {
   3038             expand_node(me, node);
   3039           }
   3040        };
   3041     node.label_div.appendChild(node.expand_toggle);
   3042 
   3043     node.plus_img = document.createElement("img");
   3044     node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png";
   3045     node.plus_img.className = "plus";
   3046     node.plus_img.width = "8";
   3047     node.plus_img.border = "0";
   3048     node.expand_toggle.appendChild(node.plus_img);
   3049 
   3050     node.expanded = false;
   3051   }
   3052 
   3053   var a = document.createElement("a");
   3054   node.label_div.appendChild(a);
   3055   node.label = document.createTextNode(text);
   3056   a.appendChild(node.label);
   3057   if (link) {
   3058     a.href = me.toroot + link;
   3059   } else {
   3060     if (children_data != null) {
   3061       a.className = "nolink";
   3062       a.href = "javascript:void(0)";
   3063       a.onclick = node.expand_toggle.onclick;
   3064       // This next line shouldn't be necessary.  I'll buy a beer for the first
   3065       // person who figures out how to remove this line and have the link
   3066       // toggle shut on the first try. --joeo (a] android.com
   3067       node.expanded = false;
   3068     }
   3069   }
   3070 
   3071 
   3072   node.children_ul = null;
   3073   node.get_children_ul = function() {
   3074       if (!node.children_ul) {
   3075         node.children_ul = document.createElement("ul");
   3076         node.children_ul.className = "children_ul";
   3077         node.children_ul.style.display = "none";
   3078         node.li.appendChild(node.children_ul);
   3079       }
   3080       return node.children_ul;
   3081     };
   3082 
   3083   return node;
   3084 }
   3085 
   3086 
   3087 
   3088 
   3089 function expand_node(me, node)
   3090 {
   3091   if (node.children_data && !node.expanded) {
   3092     if (node.children_visited) {
   3093       $(node.get_children_ul()).slideDown("fast");
   3094     } else {
   3095       get_node(me, node);
   3096       if ($(node.label_div).hasClass("absent")) {
   3097         $(node.get_children_ul()).addClass("absent");
   3098       }
   3099       $(node.get_children_ul()).slideDown("fast");
   3100     }
   3101     node.plus_img.src = me.toroot + "assets/images/triangle-opened-small.png";
   3102     node.expanded = true;
   3103 
   3104     // perform api level toggling because new nodes are new to the DOM
   3105     var selectedLevel = $("#apiLevelSelector option:selected").val();
   3106     toggleVisisbleApis(selectedLevel, "#side-nav");
   3107   }
   3108 }
   3109 
   3110 function get_node(me, mom)
   3111 {
   3112   mom.children_visited = true;
   3113   for (var i in mom.children_data) {
   3114     var node_data = mom.children_data[i];
   3115     mom.children[i] = new_node(me, mom, node_data[0], node_data[1],
   3116         node_data[2], node_data[3]);
   3117   }
   3118 }
   3119 
   3120 function this_page_relative(toroot)
   3121 {
   3122   var full = document.location.pathname;
   3123   var file = "";
   3124   if (toroot.substr(0, 1) == "/") {
   3125     if (full.substr(0, toroot.length) == toroot) {
   3126       return full.substr(toroot.length);
   3127     } else {
   3128       // the file isn't under toroot.  Fail.
   3129       return null;
   3130     }
   3131   } else {
   3132     if (toroot != "./") {
   3133       toroot = "./" + toroot;
   3134     }
   3135     do {
   3136       if (toroot.substr(toroot.length-3, 3) == "../" || toroot == "./") {
   3137         var pos = full.lastIndexOf("/");
   3138         file = full.substr(pos) + file;
   3139         full = full.substr(0, pos);
   3140         toroot = toroot.substr(0, toroot.length-3);
   3141       }
   3142     } while (toroot != "" && toroot != "/");
   3143     return file.substr(1);
   3144   }
   3145 }
   3146 
   3147 function find_page(url, data)
   3148 {
   3149   var nodes = data;
   3150   var result = null;
   3151   for (var i in nodes) {
   3152     var d = nodes[i];
   3153     if (d[1] == url) {
   3154       return new Array(i);
   3155     }
   3156     else if (d[2] != null) {
   3157       result = find_page(url, d[2]);
   3158       if (result != null) {
   3159         return (new Array(i).concat(result));
   3160       }
   3161     }
   3162   }
   3163   return null;
   3164 }
   3165 
   3166 function init_default_navtree(toroot) {
   3167   // load json file for navtree data
   3168   $.getScript(toRoot + 'navtree_data.js', function(data, textStatus, jqxhr) {
   3169       // when the file is loaded, initialize the tree
   3170       if(jqxhr.status === 200) {
   3171           init_navtree("tree-list", toroot, NAVTREE_DATA);
   3172       }
   3173   });
   3174 
   3175   // perform api level toggling because because the whole tree is new to the DOM
   3176   var selectedLevel = $("#apiLevelSelector option:selected").val();
   3177   toggleVisisbleApis(selectedLevel, "#side-nav");
   3178 }
   3179 
   3180 function init_navtree(navtree_id, toroot, root_nodes)
   3181 {
   3182   var me = new Object();
   3183   me.toroot = toroot;
   3184   me.node = new Object();
   3185 
   3186   me.node.li = document.getElementById(navtree_id);
   3187   me.node.children_data = root_nodes;
   3188   me.node.children = new Array();
   3189   me.node.children_ul = document.createElement("ul");
   3190   me.node.get_children_ul = function() { return me.node.children_ul; };
   3191   //me.node.children_ul.className = "children_ul";
   3192   me.node.li.appendChild(me.node.children_ul);
   3193   me.node.depth = 0;
   3194 
   3195   get_node(me, me.node);
   3196 
   3197   me.this_page = this_page_relative(toroot);
   3198   me.breadcrumbs = find_page(me.this_page, root_nodes);
   3199   if (me.breadcrumbs != null && me.breadcrumbs.length != 0) {
   3200     var mom = me.node;
   3201     for (var i in me.breadcrumbs) {
   3202       var j = me.breadcrumbs[i];
   3203       mom = mom.children[j];
   3204       expand_node(me, mom);
   3205     }
   3206     mom.label_div.className = mom.label_div.className + " selected";
   3207     addLoadEvent(function() {
   3208       scrollIntoView("nav-tree");
   3209       });
   3210   }
   3211 }
   3212 
   3213 
   3214 
   3215 
   3216 
   3217 
   3218 
   3219 
   3220 /* TODO: eliminate redundancy with non-google functions */
   3221 function init_google_navtree(navtree_id, toroot, root_nodes)
   3222 {
   3223   var me = new Object();
   3224   me.toroot = toroot;
   3225   me.node = new Object();
   3226 
   3227   me.node.li = document.getElementById(navtree_id);
   3228   if (!me.node.li) {
   3229     return;
   3230   }
   3231 
   3232   me.node.children_data = root_nodes;
   3233   me.node.children = new Array();
   3234   me.node.children_ul = document.createElement("ul");
   3235   me.node.get_children_ul = function() { return me.node.children_ul; };
   3236   //me.node.children_ul.className = "children_ul";
   3237   me.node.li.appendChild(me.node.children_ul);
   3238   me.node.depth = 0;
   3239 
   3240   get_google_node(me, me.node);
   3241 }
   3242 
   3243 function new_google_node(me, mom, text, link, children_data, api_level)
   3244 {
   3245   var node = new Object();
   3246   var child;
   3247   node.children = Array();
   3248   node.children_data = children_data;
   3249   node.depth = mom.depth + 1;
   3250   node.get_children_ul = function() {
   3251       if (!node.children_ul) {
   3252         node.children_ul = document.createElement("ul");
   3253         node.children_ul.className = "tree-list-children";
   3254         node.li.appendChild(node.children_ul);
   3255       }
   3256       return node.children_ul;
   3257     };
   3258   node.li = document.createElement("li");
   3259 
   3260   mom.get_children_ul().appendChild(node.li);
   3261 
   3262 
   3263   if(link) {
   3264     child = document.createElement("a");
   3265 
   3266   }
   3267   else {
   3268     child = document.createElement("span");
   3269     child.className = "tree-list-subtitle";
   3270 
   3271   }
   3272   if (children_data != null) {
   3273     node.li.className="nav-section";
   3274     node.label_div = document.createElement("div");
   3275     node.label_div.className = "nav-section-header-ref";
   3276     node.li.appendChild(node.label_div);
   3277     get_google_node(me, node);
   3278     node.label_div.appendChild(child);
   3279   }
   3280   else {
   3281     node.li.appendChild(child);
   3282   }
   3283   if(link) {
   3284     child.href = me.toroot + link;
   3285   }
   3286   node.label = document.createTextNode(text);
   3287   child.appendChild(node.label);
   3288 
   3289   node.children_ul = null;
   3290 
   3291   return node;
   3292 }
   3293 
   3294 function get_google_node(me, mom)
   3295 {
   3296   mom.children_visited = true;
   3297   var linkText;
   3298   for (var i in mom.children_data) {
   3299     var node_data = mom.children_data[i];
   3300     linkText = node_data[0];
   3301 
   3302     if(linkText.match("^"+"com.google.android")=="com.google.android"){
   3303       linkText = linkText.substr(19, linkText.length);
   3304     }
   3305       mom.children[i] = new_google_node(me, mom, linkText, node_data[1],
   3306           node_data[2], node_data[3]);
   3307   }
   3308 }
   3309 
   3310 
   3311 
   3312 
   3313 
   3314 
   3315 /****** NEW version of script to build google and sample navs dynamically ******/
   3316 // TODO: update Google reference docs to tolerate this new implementation
   3317 
   3318 var NODE_NAME = 0;
   3319 var NODE_HREF = 1;
   3320 var NODE_GROUP = 2;
   3321 var NODE_TAGS = 3;
   3322 var NODE_CHILDREN = 4;
   3323 
   3324 function init_google_navtree2(navtree_id, data)
   3325 {
   3326   var $containerUl = $("#"+navtree_id);
   3327   for (var i in data) {
   3328     var node_data = data[i];
   3329     $containerUl.append(new_google_node2(node_data));
   3330   }
   3331 
   3332   // Make all third-generation list items 'sticky' to prevent them from collapsing
   3333   $containerUl.find('li li li.nav-section').addClass('sticky');
   3334 
   3335   initExpandableNavItems("#"+navtree_id);
   3336 }
   3337 
   3338 function new_google_node2(node_data)
   3339 {
   3340   var linkText = node_data[NODE_NAME];
   3341   if(linkText.match("^"+"com.google.android")=="com.google.android"){
   3342     linkText = linkText.substr(19, linkText.length);
   3343   }
   3344   var $li = $('<li>');
   3345   var $a;
   3346   if (node_data[NODE_HREF] != null) {
   3347     $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >'
   3348         + linkText + '</a>');
   3349   } else {
   3350     $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >'
   3351         + linkText + '/</a>');
   3352   }
   3353   var $childUl = $('<ul>');
   3354   if (node_data[NODE_CHILDREN] != null) {
   3355     $li.addClass("nav-section");
   3356     $a = $('<div class="nav-section-header">').append($a);
   3357     if (node_data[NODE_HREF] == null) $a.addClass('empty');
   3358 
   3359     for (var i in node_data[NODE_CHILDREN]) {
   3360       var child_node_data = node_data[NODE_CHILDREN][i];
   3361       $childUl.append(new_google_node2(child_node_data));
   3362     }
   3363     $li.append($childUl);
   3364   }
   3365   $li.prepend($a);
   3366 
   3367   return $li;
   3368 }
   3369 
   3370 
   3371 
   3372 
   3373 
   3374 
   3375 
   3376 
   3377 
   3378 
   3379 
   3380 function showGoogleRefTree() {
   3381   init_default_google_navtree(toRoot);
   3382   init_default_gcm_navtree(toRoot);
   3383 }
   3384 
   3385 function init_default_google_navtree(toroot) {
   3386   // load json file for navtree data
   3387   $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) {
   3388       // when the file is loaded, initialize the tree
   3389       if(jqxhr.status === 200) {
   3390           init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA);
   3391           highlightSidenav();
   3392           resizeNav();
   3393       }
   3394   });
   3395 }
   3396 
   3397 function init_default_gcm_navtree(toroot) {
   3398   // load json file for navtree data
   3399   $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) {
   3400       // when the file is loaded, initialize the tree
   3401       if(jqxhr.status === 200) {
   3402           init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA);
   3403           highlightSidenav();
   3404           resizeNav();
   3405       }
   3406   });
   3407 }
   3408 
   3409 function showSamplesRefTree() {
   3410   init_default_samples_navtree(toRoot);
   3411 }
   3412 
   3413 function init_default_samples_navtree(toroot) {
   3414   // load json file for navtree data
   3415   $.getScript(toRoot + 'samples_navtree_data.js', function(data, textStatus, jqxhr) {
   3416       // when the file is loaded, initialize the tree
   3417       if(jqxhr.status === 200) {
   3418           // hack to remove the "about the samples" link then put it back in
   3419           // after we nuke the list to remove the dummy static list of samples
   3420           var $firstLi = $("#nav.samples-nav > li:first-child").clone();
   3421           $("#nav.samples-nav").empty();
   3422           $("#nav.samples-nav").append($firstLi);
   3423 
   3424           init_google_navtree2("nav.samples-nav", SAMPLES_NAVTREE_DATA);
   3425           highlightSidenav();
   3426           resizeNav();
   3427           if ($("#jd-content #samples").length) {
   3428             showSamples();
   3429           }
   3430       }
   3431   });
   3432 }
   3433 
   3434 /* TOGGLE INHERITED MEMBERS */
   3435 
   3436 /* Toggle an inherited class (arrow toggle)
   3437  * @param linkObj  The link that was clicked.
   3438  * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
   3439  *                'null' to simply toggle.
   3440  */
   3441 function toggleInherited(linkObj, expand) {
   3442     var base = linkObj.getAttribute("id");
   3443     var list = document.getElementById(base + "-list");
   3444     var summary = document.getElementById(base + "-summary");
   3445     var trigger = document.getElementById(base + "-trigger");
   3446     var a = $(linkObj);
   3447     if ( (expand == null && a.hasClass("closed")) || expand ) {
   3448         list.style.display = "none";
   3449         summary.style.display = "block";
   3450         trigger.src = toRoot + "assets/images/styles/disclosure_up.png";
   3451         a.removeClass("closed");
   3452         a.addClass("opened");
   3453     } else if ( (expand == null && a.hasClass("opened")) || (expand == false) ) {
   3454         list.style.display = "block";
   3455         summary.style.display = "none";
   3456         trigger.src = toRoot + "assets/images/styles/disclosure_down.png";
   3457         a.removeClass("opened");
   3458         a.addClass("closed");
   3459     }
   3460     return false;
   3461 }
   3462 
   3463 /* Toggle all inherited classes in a single table (e.g. all inherited methods)
   3464  * @param linkObj  The link that was clicked.
   3465  * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
   3466  *                'null' to simply toggle.
   3467  */
   3468 function toggleAllInherited(linkObj, expand) {
   3469   var a = $(linkObj);
   3470   var table = $(a.parent().parent().parent()); // ugly way to get table/tbody
   3471   var expandos = $(".jd-expando-trigger", table);
   3472   if ( (expand == null && a.text() == "[Expand]") || expand ) {
   3473     expandos.each(function(i) {
   3474       toggleInherited(this, true);
   3475     });
   3476     a.text("[Collapse]");
   3477   } else if ( (expand == null && a.text() == "[Collapse]") || (expand == false) ) {
   3478     expandos.each(function(i) {
   3479       toggleInherited(this, false);
   3480     });
   3481     a.text("[Expand]");
   3482   }
   3483   return false;
   3484 }
   3485 
   3486 /* Toggle all inherited members in the class (link in the class title)
   3487  */
   3488 function toggleAllClassInherited() {
   3489   var a = $("#toggleAllClassInherited"); // get toggle link from class title
   3490   var toggles = $(".toggle-all", $("#body-content"));
   3491   if (a.text() == "[Expand All]") {
   3492     toggles.each(function(i) {
   3493       toggleAllInherited(this, true);
   3494     });
   3495     a.text("[Collapse All]");
   3496   } else {
   3497     toggles.each(function(i) {
   3498       toggleAllInherited(this, false);
   3499     });
   3500     a.text("[Expand All]");
   3501   }
   3502   return false;
   3503 }
   3504 
   3505 /* Expand all inherited members in the class. Used when initiating page search */
   3506 function ensureAllInheritedExpanded() {
   3507   var toggles = $(".toggle-all", $("#body-content"));
   3508   toggles.each(function(i) {
   3509     toggleAllInherited(this, true);
   3510   });
   3511   $("#toggleAllClassInherited").text("[Collapse All]");
   3512 }
   3513 
   3514 
   3515 /* HANDLE KEY EVENTS
   3516  * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search)
   3517  */
   3518 var agent = navigator['userAgent'].toLowerCase();
   3519 var mac = agent.indexOf("macintosh") != -1;
   3520 
   3521 $(document).keydown( function(e) {
   3522 var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key
   3523   if (control && e.which == 70) {  // 70 is "F"
   3524     ensureAllInheritedExpanded();
   3525   }
   3526 });
   3527 
   3528 
   3529 
   3530 
   3531 
   3532 
   3533 /* On-demand functions */
   3534 
   3535 /** Move sample code line numbers out of PRE block and into non-copyable column */
   3536 function initCodeLineNumbers() {
   3537   var numbers = $("#codesample-block a.number");
   3538   if (numbers.length) {
   3539     $("#codesample-line-numbers").removeClass("hidden").append(numbers);
   3540   }
   3541 
   3542   $(document).ready(function() {
   3543     // select entire line when clicked
   3544     $("span.code-line").click(function() {
   3545       if (!shifted) {
   3546         selectText(this);
   3547       }
   3548     });
   3549     // invoke line link on double click
   3550     $(".code-line").dblclick(function() {
   3551       document.location.hash = $(this).attr('id');
   3552     });
   3553     // highlight the line when hovering on the number
   3554     $("#codesample-line-numbers a.number").mouseover(function() {
   3555       var id = $(this).attr('href');
   3556       $(id).css('background','#e7e7e7');
   3557     });
   3558     $("#codesample-line-numbers a.number").mouseout(function() {
   3559       var id = $(this).attr('href');
   3560       $(id).css('background','none');
   3561     });
   3562   });
   3563 }
   3564 
   3565 // create SHIFT key binder to avoid the selectText method when selecting multiple lines
   3566 var shifted = false;
   3567 $(document).bind('keyup keydown', function(e){shifted = e.shiftKey; return true;} );
   3568 
   3569 // courtesy of jasonedelman.com
   3570 function selectText(element) {
   3571     var doc = document
   3572         , range, selection
   3573     ;
   3574     if (doc.body.createTextRange) { //ms
   3575         range = doc.body.createTextRange();
   3576         range.moveToElementText(element);
   3577         range.select();
   3578     } else if (window.getSelection) { //all others
   3579         selection = window.getSelection();
   3580         range = doc.createRange();
   3581         range.selectNodeContents(element);
   3582         selection.removeAllRanges();
   3583         selection.addRange(range);
   3584     }
   3585 }
   3586 
   3587 
   3588 
   3589 
   3590 /** Display links and other information about samples that match the
   3591     group specified by the URL */
   3592 function showSamples() {
   3593   var group = $("#samples").attr('class');
   3594   $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>");
   3595 
   3596   var $ul = $("<ul>");
   3597   $selectedLi = $("#nav li.selected");
   3598 
   3599   $selectedLi.children("ul").children("li").each(function() {
   3600       var $li = $("<li>").append($(this).find("a").first().clone());
   3601       $ul.append($li);
   3602   });
   3603 
   3604   $("#samples").append($ul);
   3605 
   3606 }
   3607 
   3608 
   3609 
   3610 /* ########################################################## */
   3611 /* ###################  RESOURCE CARDS  ##################### */
   3612 /* ########################################################## */
   3613 
   3614 /** Handle resource queries, collections, and grids (sections). Requires
   3615     jd_tag_helpers.js and the *_unified_data.js to be loaded. */
   3616 
   3617 (function() {
   3618   // Prevent the same resource from being loaded more than once per page.
   3619   var addedPageResources = {};
   3620 
   3621   $(document).ready(function() {
   3622     // Need to initialize hero carousel before other sections for dedupe
   3623     // to work correctly.
   3624     $('[data-carousel-query]').dacCarouselQuery();
   3625 
   3626     $('.resource-widget').each(function() {
   3627       initResourceWidget(this);
   3628     });
   3629 
   3630     /* Pass the line height to ellipsisfade() to adjust the height of the
   3631     text container to show the max number of lines possible, without
   3632     showing lines that are cut off. This works with the css ellipsis
   3633     classes to fade last text line and apply an ellipsis char. */
   3634 
   3635     //card text currently uses 20px line height.
   3636     var lineHeight = 20;
   3637     $('.card-info .text').ellipsisfade(lineHeight);
   3638   });
   3639 
   3640   /*
   3641     Three types of resource layouts:
   3642     Flow - Uses a fixed row-height flow using float left style.
   3643     Carousel - Single card slideshow all same dimension absolute.
   3644     Stack - Uses fixed columns and flexible element height.
   3645   */
   3646   function initResourceWidget(widget) {
   3647     var $widget = $(widget);
   3648     var isFlow = $widget.hasClass('resource-flow-layout'),
   3649         isCarousel = $widget.hasClass('resource-carousel-layout'),
   3650         isStack = $widget.hasClass('resource-stack-layout');
   3651 
   3652     // remove illegal col-x class which is not relevant anymore thanks to responsive styles.
   3653     var m = $widget.get(0).className.match(/\bcol-(\d+)\b/);
   3654     if (m && !$widget.is('.cols > *')) {
   3655       $widget.removeClass('col-' + m[1]);
   3656     }
   3657 
   3658     var opts = {
   3659       cardSizes: ($widget.data('cardsizes') || '').split(','),
   3660       maxResults: parseInt($widget.data('maxresults') || '100', 10),
   3661       initialResults: $widget.data('initialResults'),
   3662       itemsPerPage: $widget.data('itemsperpage'),
   3663       sortOrder: $widget.data('sortorder'),
   3664       query: $widget.data('query'),
   3665       section: $widget.data('section'),
   3666       /* Added by LFL 6/6/14 */
   3667       resourceStyle: $widget.data('resourcestyle') || 'card',
   3668       stackSort: $widget.data('stacksort') || 'true'
   3669     };
   3670 
   3671     // run the search for the set of resources to show
   3672 
   3673     var resources = buildResourceList(opts);
   3674 
   3675     if (isFlow) {
   3676       drawResourcesFlowWidget($widget, opts, resources);
   3677     } else if (isCarousel) {
   3678       drawResourcesCarouselWidget($widget, opts, resources);
   3679     } else if (isStack) {
   3680       /* Looks like this got removed and is not used, so repurposing for the
   3681           homepage style layout.
   3682           Modified by LFL 6/6/14
   3683       */
   3684       //var sections = buildSectionList(opts);
   3685       opts['numStacks'] = $widget.data('numstacks');
   3686       drawResourcesStackWidget($widget, opts, resources/*, sections*/);
   3687     }
   3688   }
   3689 
   3690   /* Initializes a Resource Carousel Widget */
   3691   function drawResourcesCarouselWidget($widget, opts, resources) {
   3692     $widget.empty();
   3693     var plusone = false; // stop showing plusone buttons on cards
   3694 
   3695     $widget.addClass('resource-card slideshow-container')
   3696       .append($('<a>').addClass('slideshow-prev').text('Prev'))
   3697       .append($('<a>').addClass('slideshow-next').text('Next'));
   3698 
   3699     var css = { 'width': $widget.width() + 'px',
   3700                 'height': $widget.height() + 'px' };
   3701 
   3702     var $ul = $('<ul>');
   3703 
   3704     for (var i = 0; i < resources.length; ++i) {
   3705       var $card = $('<a>')
   3706         .attr('href', cleanUrl(resources[i].url))
   3707         .decorateResourceCard(resources[i],plusone);
   3708 
   3709       $('<li>').css(css)
   3710           .append($card)
   3711           .appendTo($ul);
   3712     }
   3713 
   3714     $('<div>').addClass('frame')
   3715       .append($ul)
   3716       .appendTo($widget);
   3717 
   3718     $widget.dacSlideshow({
   3719       auto: true,
   3720       btnPrev: '.slideshow-prev',
   3721       btnNext: '.slideshow-next'
   3722     });
   3723   };
   3724 
   3725   /* Initializes a Resource Card Stack Widget (column-based layout)
   3726      Modified by LFL 6/6/14
   3727    */
   3728   function drawResourcesStackWidget($widget, opts, resources, sections) {
   3729     // Don't empty widget, grab all items inside since they will be the first
   3730     // items stacked, followed by the resource query
   3731     var plusone = false; // stop showing plusone buttons on cards
   3732     var cards = $widget.find('.resource-card').detach().toArray();
   3733     var numStacks = opts.numStacks || 1;
   3734     var $stacks = [];
   3735     var urlString;
   3736 
   3737     for (var i = 0; i < numStacks; ++i) {
   3738       $stacks[i] = $('<div>').addClass('resource-card-stack')
   3739           .appendTo($widget);
   3740     }
   3741 
   3742     var sectionResources = [];
   3743 
   3744     // Extract any subsections that are actually resource cards
   3745     if (sections) {
   3746       for (var i = 0; i < sections.length; ++i) {
   3747         if (!sections[i].sections || !sections[i].sections.length) {
   3748           // Render it as a resource card
   3749           sectionResources.push(
   3750             $('<a>')
   3751               .addClass('resource-card section-card')
   3752               .attr('href', cleanUrl(sections[i].resource.url))
   3753               .decorateResourceCard(sections[i].resource,plusone)[0]
   3754           );
   3755 
   3756         } else {
   3757           cards.push(
   3758             $('<div>')
   3759               .addClass('resource-card section-card-menu')
   3760               .decorateResourceSection(sections[i],plusone)[0]
   3761           );
   3762         }
   3763       }
   3764     }
   3765 
   3766     cards = cards.concat(sectionResources);
   3767 
   3768     for (var i = 0; i < resources.length; ++i) {
   3769       var $card = createResourceElement(resources[i], opts);
   3770 
   3771       if (opts.resourceStyle.indexOf('related') > -1) {
   3772         $card.addClass('related-card');
   3773       }
   3774 
   3775       cards.push($card[0]);
   3776     }
   3777 
   3778     if (opts.stackSort != 'false') {
   3779       for (var i = 0; i < cards.length; ++i) {
   3780         // Find the stack with the shortest height, but give preference to
   3781         // left to right order.
   3782         var minHeight = $stacks[0].height();
   3783         var minIndex = 0;
   3784 
   3785         for (var j = 1; j < numStacks; ++j) {
   3786           var height = $stacks[j].height();
   3787           if (height < minHeight - 45) {
   3788             minHeight = height;
   3789             minIndex = j;
   3790           }
   3791         }
   3792 
   3793         $stacks[minIndex].append($(cards[i]));
   3794       }
   3795     }
   3796 
   3797   };
   3798 
   3799   /*
   3800     Create a resource card using the given resource object and a list of html
   3801      configured options. Returns a jquery object containing the element.
   3802   */
   3803   function createResourceElement(resource, opts, plusone) {
   3804     var $el;
   3805 
   3806     // The difference here is that generic cards are not entirely clickable
   3807     // so its a div instead of an a tag, also the generic one is not given
   3808     // the resource-card class so it appears with a transparent background
   3809     // and can be styled in whatever way the css setup.
   3810     if (opts.resourceStyle == 'generic') {
   3811       $el = $('<div>')
   3812         .addClass('resource')
   3813         .attr('href', cleanUrl(resource.url))
   3814         .decorateResource(resource, opts);
   3815     } else {
   3816       var cls = 'resource resource-card';
   3817 
   3818       $el = $('<a>')
   3819         .addClass(cls)
   3820         .attr('href', cleanUrl(resource.url))
   3821         .decorateResourceCard(resource, plusone);
   3822     }
   3823 
   3824     return $el;
   3825   }
   3826 
   3827   function createResponsiveFlowColumn(cardSize) {
   3828     var cardWidth = parseInt(cardSize.match(/(\d+)/)[1], 10);
   3829     var column = $('<div>').addClass('col-' + (cardWidth / 3) + 'of6');
   3830     if (cardWidth < 9) {
   3831       column.addClass('col-tablet-1of2');
   3832     } else if (cardWidth > 9 && cardWidth < 18) {
   3833       column.addClass('col-tablet-1of1');
   3834     }
   3835     if (cardWidth < 18) {
   3836       column.addClass('col-mobile-1of1')
   3837     }
   3838     return column;
   3839   }
   3840 
   3841   /* Initializes a flow widget, see distribute.scss for generating accompanying css */
   3842   function drawResourcesFlowWidget($widget, opts, resources) {
   3843     $widget.empty().addClass('cols');
   3844     var cardSizes = opts.cardSizes || ['6x6'];
   3845     var initialResults = opts.initialResults || resources.length;
   3846     var i = 0, j = 0;
   3847     var plusone = false; // stop showing plusone buttons on cards
   3848     var cardParent = $widget;
   3849 
   3850     while (i < resources.length) {
   3851 
   3852       if (i === initialResults && initialResults < resources.length) {
   3853         // Toggle remaining cards
   3854         cardParent = $('<div class="dac-toggle-content clearfix">').appendTo($widget);
   3855         $widget.addClass('dac-toggle');
   3856         $('<div class="col-1of1 dac-section-links dac-text-center">')
   3857           .append(
   3858             $('<div class="dac-section-link" data-toggle="section">')
   3859               .append('<span class="dac-toggle-expand">More<i class="dac-sprite dac-auto-unfold-more"></i></span>')
   3860               .append('<span class="dac-toggle-collapse">Less<i class="dac-sprite dac-auto-unfold-less"></i></span>')
   3861           )
   3862           .appendTo($widget)
   3863       }
   3864 
   3865       var cardSize = cardSizes[j++ % cardSizes.length];
   3866       cardSize = cardSize.replace(/^\s+|\s+$/,'');
   3867 
   3868       var column = createResponsiveFlowColumn(cardSize).appendTo(cardParent);
   3869 
   3870       // A stack has a third dimension which is the number of stacked items
   3871       var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/);
   3872       var stackCount = 0;
   3873       var $stackDiv = null;
   3874 
   3875       if (isStack) {
   3876         // Create a stack container which should have the dimensions defined
   3877         // by the product of the items inside.
   3878         $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1]
   3879             + 'x' + isStack[2] * isStack[3]) .appendTo(column);
   3880       }
   3881 
   3882       // Build each stack item or just a single item
   3883       do {
   3884         var resource = resources[i];
   3885 
   3886         var $card = createResourceElement(resources[i], opts, plusone);
   3887 
   3888         $card.addClass('resource-card-' + cardSize +
   3889           ' resource-card-' + resource.type);
   3890 
   3891         if (isStack) {
   3892           $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]);
   3893           if (++stackCount == parseInt(isStack[3])) {
   3894             $card.addClass('resource-card-row-stack-last');
   3895             stackCount = 0;
   3896           }
   3897         } else {
   3898           stackCount = 0;
   3899         }
   3900 
   3901         $card.appendTo($stackDiv || column);
   3902 
   3903       } while (++i < resources.length && stackCount > 0);
   3904     }
   3905   }
   3906 
   3907   /* Build a site map of resources using a section as a root. */
   3908   function buildSectionList(opts) {
   3909     if (opts.section && SECTION_BY_ID[opts.section]) {
   3910       return SECTION_BY_ID[opts.section].sections || [];
   3911     }
   3912     return [];
   3913   }
   3914 
   3915   function buildResourceList(opts) {
   3916     return $.queryResources(opts);
   3917   }
   3918 
   3919   $.queryResources = function(opts) {
   3920     var maxResults = opts.maxResults || 100;
   3921 
   3922     var query = opts.query || '';
   3923     var expressions = parseResourceQuery(query);
   3924     var addedResourceIndices = {};
   3925     var results = [];
   3926 
   3927     for (var i = 0; i < expressions.length; i++) {
   3928       var clauses = expressions[i];
   3929 
   3930       // build initial set of resources from first clause
   3931       var firstClause = clauses[0];
   3932       var resources = [];
   3933       switch (firstClause.attr) {
   3934         case 'type':
   3935           resources = ALL_RESOURCES_BY_TYPE[firstClause.value];
   3936           break;
   3937         case 'lang':
   3938           resources = ALL_RESOURCES_BY_LANG[firstClause.value];
   3939           break;
   3940         case 'tag':
   3941           resources = ALL_RESOURCES_BY_TAG[firstClause.value];
   3942           break;
   3943         case 'collection':
   3944           var urls = RESOURCE_COLLECTIONS[firstClause.value].resources || [];
   3945           resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; });
   3946           break;
   3947         case 'section':
   3948           var urls = SITE_MAP[firstClause.value].sections || [];
   3949           resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; });
   3950           break;
   3951       }
   3952       // console.log(firstClause.attr + ':' + firstClause.value);
   3953       resources = resources || [];
   3954 
   3955       // use additional clauses to filter corpus
   3956       if (clauses.length > 1) {
   3957         var otherClauses = clauses.slice(1);
   3958         resources = resources.filter(getResourceMatchesClausesFilter(otherClauses));
   3959       }
   3960 
   3961       // filter out resources already added
   3962       if (i > 1) {
   3963         resources = resources.filter(getResourceNotAlreadyAddedFilter(addedResourceIndices));
   3964       }
   3965 
   3966       // add to list of already added indices
   3967       for (var j = 0; j < resources.length; j++) {
   3968         if (resources[j]) {
   3969           addedResourceIndices[resources[j].index] = 1;
   3970         }
   3971       }
   3972 
   3973       // concat to final results list
   3974       results = results.concat(resources);
   3975     }
   3976 
   3977     if (opts.sortOrder && results.length) {
   3978       var attr = opts.sortOrder;
   3979 
   3980       if (opts.sortOrder == 'random') {
   3981         var i = results.length, j, temp;
   3982         while (--i) {
   3983           j = Math.floor(Math.random() * (i + 1));
   3984           temp = results[i];
   3985           results[i] = results[j];
   3986           results[j] = temp;
   3987         }
   3988       } else {
   3989         var desc = attr.charAt(0) == '-';
   3990         if (desc) {
   3991           attr = attr.substring(1);
   3992         }
   3993         results = results.sort(function(x,y) {
   3994           return (desc ? -1 : 1) * (parseInt(x[attr], 10) - parseInt(y[attr], 10));
   3995         });
   3996       }
   3997     }
   3998 
   3999     results = results.filter(getResourceNotAlreadyAddedFilter(addedPageResources));
   4000     results = results.slice(0, maxResults);
   4001 
   4002     for (var j = 0; j < results.length; ++j) {
   4003       addedPageResources[results[j].index] = 1;
   4004     }
   4005 
   4006     return results;
   4007   }
   4008 
   4009 
   4010   function getResourceNotAlreadyAddedFilter(addedResourceIndices) {
   4011     return function(resource) {
   4012       return resource && !addedResourceIndices[resource.index];
   4013     };
   4014   }
   4015 
   4016 
   4017   function getResourceMatchesClausesFilter(clauses) {
   4018     return function(resource) {
   4019       return doesResourceMatchClauses(resource, clauses);
   4020     };
   4021   }
   4022 
   4023 
   4024   function doesResourceMatchClauses(resource, clauses) {
   4025     for (var i = 0; i < clauses.length; i++) {
   4026       var map;
   4027       switch (clauses[i].attr) {
   4028         case 'type':
   4029           map = IS_RESOURCE_OF_TYPE[clauses[i].value];
   4030           break;
   4031         case 'lang':
   4032           map = IS_RESOURCE_IN_LANG[clauses[i].value];
   4033           break;
   4034         case 'tag':
   4035           map = IS_RESOURCE_TAGGED[clauses[i].value];
   4036           break;
   4037       }
   4038 
   4039       if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) {
   4040         return clauses[i].negative;
   4041       }
   4042     }
   4043     return true;
   4044   }
   4045 
   4046   function cleanUrl(url)
   4047   {
   4048     if (url && url.indexOf('//') === -1) {
   4049       url = toRoot + url;
   4050     }
   4051 
   4052     return url;
   4053   }
   4054 
   4055 
   4056   function parseResourceQuery(query) {
   4057     // Parse query into array of expressions (expression e.g. 'tag:foo + type:video')
   4058     var expressions = [];
   4059     var expressionStrs = query.split(',') || [];
   4060     for (var i = 0; i < expressionStrs.length; i++) {
   4061       var expr = expressionStrs[i] || '';
   4062 
   4063       // Break expression into clauses (clause e.g. 'tag:foo')
   4064       var clauses = [];
   4065       var clauseStrs = expr.split(/(?=[\+\-])/);
   4066       for (var j = 0; j < clauseStrs.length; j++) {
   4067         var clauseStr = clauseStrs[j] || '';
   4068 
   4069         // Get attribute and value from clause (e.g. attribute='tag', value='foo')
   4070         var parts = clauseStr.split(':');
   4071         var clause = {};
   4072 
   4073         clause.attr = parts[0].replace(/^\s+|\s+$/g,'');
   4074         if (clause.attr) {
   4075           if (clause.attr.charAt(0) == '+') {
   4076             clause.attr = clause.attr.substring(1);
   4077           } else if (clause.attr.charAt(0) == '-') {
   4078             clause.negative = true;
   4079             clause.attr = clause.attr.substring(1);
   4080           }
   4081         }
   4082 
   4083         if (parts.length > 1) {
   4084           clause.value = parts[1].replace(/^\s+|\s+$/g,'');
   4085         }
   4086 
   4087         clauses.push(clause);
   4088       }
   4089 
   4090       if (!clauses.length) {
   4091         continue;
   4092       }
   4093 
   4094       expressions.push(clauses);
   4095     }
   4096 
   4097     return expressions;
   4098   }
   4099 })();
   4100 
   4101 (function($) {
   4102 
   4103   /*
   4104     Utility method for creating dom for the description area of a card.
   4105     Used in decorateResourceCard and decorateResource.
   4106   */
   4107   function buildResourceCardDescription(resource, plusone) {
   4108     var $description = $('<div>').addClass('description ellipsis');
   4109 
   4110     $description.append($('<div>').addClass('text').html(resource.summary));
   4111 
   4112     if (resource.cta) {
   4113       $description.append($('<a>').addClass('cta').html(resource.cta));
   4114     }
   4115 
   4116     if (plusone) {
   4117       var plusurl = resource.url.indexOf("//") > -1 ? resource.url :
   4118         "//developer.android.com/" + resource.url;
   4119 
   4120       $description.append($('<div>').addClass('util')
   4121         .append($('<div>').addClass('g-plusone')
   4122           .attr('data-size', 'small')
   4123           .attr('data-align', 'right')
   4124           .attr('data-href', plusurl)));
   4125     }
   4126 
   4127     return $description;
   4128   }
   4129 
   4130 
   4131   /* Simple jquery function to create dom for a standard resource card */
   4132   $.fn.decorateResourceCard = function(resource,plusone) {
   4133     var section = resource.group || resource.type;
   4134     var imgUrl = resource.image ||
   4135       'assets/images/resource-card-default-android.jpg';
   4136 
   4137     if (imgUrl.indexOf('//') === -1) {
   4138       imgUrl = toRoot + imgUrl;
   4139     }
   4140 
   4141     if (resource.type === 'youtube') {
   4142       $('<div>').addClass('play-button')
   4143         .append($('<i class="dac-sprite dac-play-white">'))
   4144         .appendTo(this);
   4145     }
   4146 
   4147     $('<div>').addClass('card-bg')
   4148       .css('background-image', 'url(' + (imgUrl || toRoot +
   4149         'assets/images/resource-card-default-android.jpg') + ')')
   4150       .appendTo(this);
   4151 
   4152     $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : ''))
   4153       .append($('<div>').addClass('section').text(section))
   4154       .append($('<div>').addClass('title').html(resource.title))
   4155       .append(buildResourceCardDescription(resource, plusone))
   4156       .appendTo(this);
   4157 
   4158     return this;
   4159   };
   4160 
   4161   /* Simple jquery function to create dom for a resource section card (menu) */
   4162   $.fn.decorateResourceSection = function(section,plusone) {
   4163     var resource = section.resource;
   4164     //keep url clean for matching and offline mode handling
   4165     var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot;
   4166     var $base = $('<a>')
   4167         .addClass('card-bg')
   4168         .attr('href', resource.url)
   4169         .append($('<div>').addClass('card-section-icon')
   4170           .append($('<div>').addClass('icon'))
   4171           .append($('<div>').addClass('section').html(resource.title)))
   4172       .appendTo(this);
   4173 
   4174     var $cardInfo = $('<div>').addClass('card-info').appendTo(this);
   4175 
   4176     if (section.sections && section.sections.length) {
   4177       // Recurse the section sub-tree to find a resource image.
   4178       var stack = [section];
   4179 
   4180       while (stack.length) {
   4181         if (stack[0].resource.image) {
   4182           $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')');
   4183           break;
   4184         }
   4185 
   4186         if (stack[0].sections) {
   4187           stack = stack.concat(stack[0].sections);
   4188         }
   4189 
   4190         stack.shift();
   4191       }
   4192 
   4193       var $ul = $('<ul>')
   4194         .appendTo($cardInfo);
   4195 
   4196       var max = section.sections.length > 3 ? 3 : section.sections.length;
   4197 
   4198       for (var i = 0; i < max; ++i) {
   4199 
   4200         var subResource = section.sections[i];
   4201         if (!plusone) {
   4202           $('<li>')
   4203             .append($('<a>').attr('href', subResource.url)
   4204               .append($('<div>').addClass('title').html(subResource.title))
   4205               .append($('<div>').addClass('description ellipsis')
   4206                 .append($('<div>').addClass('text').html(subResource.summary))
   4207                 .append($('<div>').addClass('util'))))
   4208           .appendTo($ul);
   4209         } else {
   4210           $('<li>')
   4211             .append($('<a>').attr('href', subResource.url)
   4212               .append($('<div>').addClass('title').html(subResource.title))
   4213               .append($('<div>').addClass('description ellipsis')
   4214                 .append($('<div>').addClass('text').html(subResource.summary))
   4215                 .append($('<div>').addClass('util')
   4216                   .append($('<div>').addClass('g-plusone')
   4217                     .attr('data-size', 'small')
   4218                     .attr('data-align', 'right')
   4219                     .attr('data-href', resource.url)))))
   4220           .appendTo($ul);
   4221         }
   4222       }
   4223 
   4224       // Add a more row
   4225       if (max < section.sections.length) {
   4226         $('<li>')
   4227           .append($('<a>').attr('href', resource.url)
   4228             .append($('<div>')
   4229               .addClass('title')
   4230               .text('More')))
   4231         .appendTo($ul);
   4232       }
   4233     } else {
   4234       // No sub-resources, just render description?
   4235     }
   4236 
   4237     return this;
   4238   };
   4239 
   4240 
   4241 
   4242 
   4243   /* Render other types of resource styles that are not cards. */
   4244   $.fn.decorateResource = function(resource, opts) {
   4245     var imgUrl = resource.image ||
   4246       'assets/images/resource-card-default-android.jpg';
   4247     var linkUrl = resource.url;
   4248 
   4249     if (imgUrl.indexOf('//') === -1) {
   4250       imgUrl = toRoot + imgUrl;
   4251     }
   4252 
   4253     if (linkUrl && linkUrl.indexOf('//') === -1) {
   4254       linkUrl = toRoot + linkUrl;
   4255     }
   4256 
   4257     $(this).append(
   4258       $('<div>').addClass('image')
   4259         .css('background-image', 'url(' + imgUrl + ')'),
   4260       $('<div>').addClass('info').append(
   4261         $('<h4>').addClass('title').html(resource.title),
   4262         $('<p>').addClass('summary').html(resource.summary),
   4263         $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More')
   4264       )
   4265     );
   4266 
   4267     return this;
   4268   };
   4269 })(jQuery);
   4270 
   4271 
   4272 /* Calculate the vertical area remaining */
   4273 (function($) {
   4274     $.fn.ellipsisfade= function(lineHeight) {
   4275         this.each(function() {
   4276             // get element text
   4277             var $this = $(this);
   4278             var remainingHeight = $this.parent().parent().height();
   4279             $this.parent().siblings().each(function ()
   4280             {
   4281               if ($(this).is(":visible")) {
   4282                 var h = $(this).outerHeight(true);
   4283                 remainingHeight = remainingHeight - h;
   4284               }
   4285             });
   4286 
   4287             adjustedRemainingHeight = ((remainingHeight)/lineHeight>>0)*lineHeight
   4288             $this.parent().css({'height': adjustedRemainingHeight});
   4289             $this.css({'height': "auto"});
   4290         });
   4291 
   4292         return this;
   4293     };
   4294 }) (jQuery);
   4295 
   4296 /*
   4297   Fullscreen Carousel
   4298 
   4299   The following allows for an area at the top of the page that takes over the
   4300   entire browser height except for its top offset and an optional bottom
   4301   padding specified as a data attribute.
   4302 
   4303   HTML:
   4304 
   4305   <div class="fullscreen-carousel">
   4306     <div class="fullscreen-carousel-content">
   4307       <!-- content here -->
   4308     </div>
   4309     <div class="fullscreen-carousel-content">
   4310       <!-- content here -->
   4311     </div>
   4312 
   4313     etc ...
   4314 
   4315   </div>
   4316 
   4317   Control over how the carousel takes over the screen can mostly be defined in
   4318   a css file. Setting min-height on the .fullscreen-carousel-content elements
   4319   will prevent them from shrinking to far vertically when the browser is very
   4320   short, and setting max-height on the .fullscreen-carousel itself will prevent
   4321   the area from becoming to long in the case that the browser is stretched very
   4322   tall.
   4323 
   4324   There is limited functionality for having multiple sections since that request
   4325   was removed, but it is possible to add .next-arrow and .prev-arrow elements to
   4326   scroll between multiple content areas.
   4327 */
   4328 
   4329 (function() {
   4330   $(document).ready(function() {
   4331     $('.fullscreen-carousel').each(function() {
   4332       initWidget(this);
   4333     });
   4334   });
   4335 
   4336   function initWidget(widget) {
   4337     var $widget = $(widget);
   4338 
   4339     var topOffset = $widget.offset().top;
   4340     var padBottom = parseInt($widget.data('paddingbottom')) || 0;
   4341     var maxHeight = 0;
   4342     var minHeight = 0;
   4343     var $content = $widget.find('.fullscreen-carousel-content');
   4344     var $nextArrow = $widget.find('.next-arrow');
   4345     var $prevArrow = $widget.find('.prev-arrow');
   4346     var $curSection = $($content[0]);
   4347 
   4348     if ($content.length <= 1) {
   4349       $nextArrow.hide();
   4350       $prevArrow.hide();
   4351     } else {
   4352       $nextArrow.click(function() {
   4353         var index = ($content.index($curSection) + 1);
   4354         $curSection.hide();
   4355         $curSection = $($content[index >= $content.length ? 0 : index]);
   4356         $curSection.show();
   4357       });
   4358 
   4359       $prevArrow.click(function() {
   4360         var index = ($content.index($curSection) - 1);
   4361         $curSection.hide();
   4362         $curSection = $($content[index < 0 ? $content.length - 1 : 0]);
   4363         $curSection.show();
   4364       });
   4365     }
   4366 
   4367     // Just hide all content sections except first.
   4368     $content.each(function(index) {
   4369       if ($(this).height() > minHeight) minHeight = $(this).height();
   4370       $(this).css({position: 'absolute',  display: index > 0 ? 'none' : ''});
   4371     });
   4372 
   4373     // Register for changes to window size, and trigger.
   4374     $(window).resize(resizeWidget);
   4375     resizeWidget();
   4376 
   4377     function resizeWidget() {
   4378       var height = $(window).height() - topOffset - padBottom;
   4379       $widget.width($(window).width());
   4380       $widget.height(height < minHeight ? minHeight :
   4381         (maxHeight && height > maxHeight ? maxHeight : height));
   4382     }
   4383   }
   4384 })();
   4385 
   4386 
   4387 
   4388 
   4389 
   4390 /*
   4391   Tab Carousel
   4392 
   4393   The following allows tab widgets to be installed via the html below. Each
   4394   tab content section should have a data-tab attribute matching one of the
   4395   nav items'. Also each tab content section should have a width matching the
   4396   tab carousel.
   4397 
   4398   HTML:
   4399 
   4400   <div class="tab-carousel">
   4401     <ul class="tab-nav">
   4402       <li><a href="#" data-tab="handsets">Handsets</a>
   4403       <li><a href="#" data-tab="wearable">Wearable</a>
   4404       <li><a href="#" data-tab="tv">TV</a>
   4405     </ul>
   4406 
   4407     <div class="tab-carousel-content">
   4408       <div data-tab="handsets">
   4409         <!--Full width content here-->
   4410       </div>
   4411 
   4412       <div data-tab="wearable">
   4413         <!--Full width content here-->
   4414       </div>
   4415 
   4416       <div data-tab="tv">
   4417         <!--Full width content here-->
   4418       </div>
   4419     </div>
   4420   </div>
   4421 
   4422 */
   4423 (function() {
   4424   $(document).ready(function() {
   4425     $('.tab-carousel').each(function() {
   4426       initWidget(this);
   4427     });
   4428   });
   4429 
   4430   function initWidget(widget) {
   4431     var $widget = $(widget);
   4432     var $nav = $widget.find('.tab-nav');
   4433     var $anchors = $nav.find('[data-tab]');
   4434     var $li = $nav.find('li');
   4435     var $contentContainer = $widget.find('.tab-carousel-content');
   4436     var $tabs = $contentContainer.find('[data-tab]');
   4437     var $curTab = $($tabs[0]); // Current tab is first tab.
   4438     var width = $widget.width();
   4439 
   4440     // Setup nav interactivity.
   4441     $anchors.click(function(evt) {
   4442       evt.preventDefault();
   4443       var query = '[data-tab=' + $(this).data('tab') + ']';
   4444       transitionWidget($tabs.filter(query));
   4445     });
   4446 
   4447     // Add highlight for navigation on first item.
   4448     var $highlight = $('<div>').addClass('highlight')
   4449       .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'})
   4450       .appendTo($nav);
   4451 
   4452     // Store height since we will change contents to absolute.
   4453     $contentContainer.height($contentContainer.height());
   4454 
   4455     // Absolutely position tabs so they're ready for transition.
   4456     $tabs.each(function(index) {
   4457       $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'});
   4458     });
   4459 
   4460     function transitionWidget($toTab) {
   4461       if (!$curTab.is($toTab)) {
   4462         var curIndex = $tabs.index($curTab[0]);
   4463         var toIndex = $tabs.index($toTab[0]);
   4464         var dir = toIndex > curIndex ? 1 : -1;
   4465 
   4466         // Animate content sections.
   4467         $toTab.css({left:(width * dir) + 'px'});
   4468         $curTab.animate({left:(width * -dir) + 'px'});
   4469         $toTab.animate({left:'0'});
   4470 
   4471         // Animate navigation highlight.
   4472         $highlight.animate({left:$($li[toIndex]).position().left + 'px',
   4473           width:$($li[toIndex]).outerWidth() + 'px'})
   4474 
   4475         // Store new current section.
   4476         $curTab = $toTab;
   4477       }
   4478     }
   4479   }
   4480 })();
   4481 
   4482 /**
   4483  * Auto TOC
   4484  *
   4485  * Upgrades h2s on the page to have a rule and be toggle-able on mobile.
   4486  */
   4487 (function($) {
   4488   var upgraded = false;
   4489   var h2Titles;
   4490 
   4491   function initWidget() {
   4492     // add HRs below all H2s (except for a few other h2 variants)
   4493     // Consider doing this with css instead.
   4494     h2Titles = $('h2').not('#qv h2, #tb h2, .sidebox h2, #devdoc-nav h2, h2.norule');
   4495     h2Titles.css({marginBottom:0}).after('<hr/>');
   4496 
   4497     // Exit early if on older browser.
   4498     if (!window.matchMedia) {
   4499       return;
   4500     }
   4501 
   4502     // Only run logic in mobile layout.
   4503     var query = window.matchMedia('(max-width: 719px)');
   4504     if (query.matches) {
   4505       makeTogglable();
   4506     } else {
   4507       query.addListener(makeTogglable);
   4508     }
   4509   }
   4510 
   4511   function makeTogglable() {
   4512     // Only run this logic once.
   4513     if (upgraded) { return; }
   4514     upgraded = true;
   4515 
   4516     // Only make content h2s togglable.
   4517     var contentTitles = h2Titles.filter('#jd-content *');
   4518 
   4519     // If there are more than 1
   4520     if (contentTitles.size() < 2) {
   4521       return;
   4522     }
   4523 
   4524     contentTitles.each(function() {
   4525       // Find all the relevant nodes.
   4526       var $title = $(this);
   4527       var $hr = $title.next();
   4528       var $contents = $hr.nextUntil('h2, .next-docs');
   4529       var $section = $($title)
   4530         .add($hr)
   4531         .add($title.prev('a[name]'))
   4532         .add($contents);
   4533       var $anchor = $section.first().prev();
   4534       var anchorMethod = 'after';
   4535       if ($anchor.length === 0) {
   4536         $anchor = $title.parent();
   4537         anchorMethod = 'prepend';
   4538       }
   4539 
   4540       // Some h2s are in their own container making it pretty hard to find the end, so skip.
   4541       if ($contents.length === 0) {
   4542         return;
   4543       }
   4544 
   4545       // Remove from DOM before messing with it. DOM is slow!
   4546       $section.detach();
   4547 
   4548       // Add mobile-only expand arrows.
   4549       $title.prepend('<span class="dac-visible-mobile-inline-block">' +
   4550           '<i class="dac-toggle-expand dac-sprite dac-expand-more-black"></i>' +
   4551           '<i class="dac-toggle-collapse dac-sprite dac-expand-less-black"></i>' +
   4552           '</span>')
   4553         .attr('data-toggle', 'section');
   4554 
   4555       // Wrap in magic markup.
   4556       $section = $section.wrapAll('<div class="dac-toggle dac-mobile">').parent();
   4557       $contents.wrapAll('<div class="dac-toggle-content"><div>'); // extra div used for max-height calculation.
   4558 
   4559       // Pre-expand section if requested.
   4560       if ($title.hasClass('is-expanded')) {
   4561         $section.addClass('is-expanded');
   4562       }
   4563 
   4564       // Pre-expand section if targetted by hash.
   4565       if (location.hash && $section.find(location.hash).length) {
   4566         $section.addClass('is-expanded');
   4567       }
   4568 
   4569       // Add it back to the dom.
   4570       $anchor[anchorMethod].call($anchor, $section);
   4571     });
   4572   }
   4573 
   4574   $(function() {
   4575     initWidget();
   4576   });
   4577 })(jQuery);
   4578 
   4579 (function($) {
   4580   'use strict';
   4581 
   4582   /**
   4583    * Toggle Floating Label state.
   4584    * @param {HTMLElement} el - The DOM element.
   4585    * @param options
   4586    * @constructor
   4587    */
   4588   function FloatingLabel(el, options) {
   4589     this.el = $(el);
   4590     this.options = $.extend({}, FloatingLabel.DEFAULTS_, options);
   4591     this.group = this.el.closest('.dac-form-input-group');
   4592     this.input = this.group.find('.dac-form-input');
   4593 
   4594     this.checkValue_ = this.checkValue_.bind(this);
   4595     this.checkValue_();
   4596 
   4597     this.input.on('focus', function() {
   4598       this.group.addClass('dac-focused');
   4599     }.bind(this));
   4600     this.input.on('blur', function() {
   4601       this.group.removeClass('dac-focused');
   4602       this.checkValue_();
   4603     }.bind(this));
   4604     this.input.on('keyup', this.checkValue_);
   4605   }
   4606 
   4607   /**
   4608    * The label is moved out of the textbox when it has a value.
   4609    */
   4610   FloatingLabel.prototype.checkValue_ = function() {
   4611     if (this.input.val().length) {
   4612       this.group.addClass('dac-has-value');
   4613     } else {
   4614       this.group.removeClass('dac-has-value');
   4615     }
   4616   };
   4617 
   4618   /**
   4619    * jQuery plugin
   4620    * @param  {object} options - Override default options.
   4621    */
   4622   $.fn.dacFloatingLabel = function(options) {
   4623     return this.each(function() {
   4624       new FloatingLabel(this, options);
   4625     });
   4626   };
   4627 
   4628   $(document).on('ready.aranja', function() {
   4629     $('.dac-form-floatlabel').each(function() {
   4630       $(this).dacFloatingLabel($(this).data());
   4631     });
   4632   });
   4633 })(jQuery);
   4634 
   4635 /* global toRoot, CAROUSEL_OVERRIDE */
   4636 (function($) {
   4637   // Ordering matters
   4638   var TAG_MAP = [
   4639     {from: 'developerstory', to: 'Android Developer Story'},
   4640     {from: 'googleplay', to: 'Google Play'}
   4641   ];
   4642 
   4643   function DacCarouselQuery(el) {
   4644     this.el = $(el);
   4645 
   4646     var opts = this.el.data();
   4647     opts.maxResults = parseInt(opts.maxResults || '100', 10);
   4648     opts.query = opts.carouselQuery;
   4649     var resources = $.queryResources(opts);
   4650 
   4651     this.el.empty();
   4652     $(resources).map(function() {
   4653       var resource = $.extend({}, this, CAROUSEL_OVERRIDE[this.url]);
   4654       var slide = $('<article class="dac-expand dac-hero">');
   4655       var image = cleanUrl(resource.heroImage || resource.image);
   4656       var fullBleed = image && !resource.heroColor;
   4657 
   4658       // Configure background
   4659       slide.css({
   4660         backgroundImage: fullBleed ? 'url(' + image + ')' : '',
   4661         backgroundColor: resource.heroColor || ''
   4662       });
   4663 
   4664       // Should copy be inverted
   4665       slide.toggleClass('dac-invert', resource.heroInvert || fullBleed);
   4666       slide.toggleClass('dac-darken', fullBleed);
   4667 
   4668       // Should be clickable
   4669       slide.append($('<a class="dac-hero-carousel-action">').attr('href', cleanUrl(resource.url)));
   4670 
   4671       var cols = $('<div class="cols dac-hero-content">');
   4672 
   4673       // inline image column
   4674       var rightCol = $('<div class="col-1of2 col-push-1of2 dac-hero-figure">')
   4675         .appendTo(cols);
   4676 
   4677       if (!fullBleed && image) {
   4678         rightCol.append($('<img>').attr('src', image));
   4679       }
   4680 
   4681       // info column
   4682       $('<div class="col-1of2 col-pull-1of2">')
   4683         .append($('<div class="dac-hero-tag">').text(formatTag(resource)))
   4684         .append($('<h1 class="dac-hero-title">').text(formatTitle(resource)))
   4685         .append($('<p class="dac-hero-description">').text(resource.summary))
   4686         .append($('<a class="dac-hero-cta">')
   4687           .text(formatCTA(resource))
   4688           .attr('href', cleanUrl(resource.url))
   4689           .prepend($('<span class="dac-sprite dac-auto-chevron">'))
   4690         )
   4691         .appendTo(cols);
   4692 
   4693       slide.append(cols.wrap('<div class="wrap">').parent());
   4694       return slide[0];
   4695     }).prependTo(this.el);
   4696 
   4697     // Pagination element.
   4698     this.el.append('<div class="dac-hero-carousel-pagination"><div class="wrap" data-carousel-pagination>');
   4699 
   4700     this.el.dacCarousel();
   4701   }
   4702 
   4703   function cleanUrl(url) {
   4704     if (url && url.indexOf('//') === -1) {
   4705       url = toRoot + url;
   4706     }
   4707     return url;
   4708   }
   4709 
   4710   function formatTag(resource) {
   4711     // Hmm, need a better more scalable solution for this.
   4712     for (var i = 0, mapping; mapping = TAG_MAP[i]; i++) {
   4713       if (resource.tags.indexOf(mapping.from) > -1) {
   4714         return mapping.to;
   4715       }
   4716     }
   4717     return resource.type;
   4718   }
   4719 
   4720   function formatTitle(resource) {
   4721     return resource.title.replace(/android developer story: /i, '');
   4722   }
   4723 
   4724   function formatCTA(resource) {
   4725     return resource.type === 'youtube' ? 'Watch the video' : 'Learn more';
   4726   }
   4727 
   4728   // jQuery plugin
   4729   $.fn.dacCarouselQuery = function() {
   4730     return this.each(function() {
   4731       var el = $(this);
   4732       var data = el.data('dac.carouselQuery');
   4733 
   4734       if (!data) { el.data('dac.carouselQuery', (data = new DacCarouselQuery(el))); }
   4735     });
   4736   };
   4737 
   4738   // Data API
   4739   $(function() {
   4740     $('[data-carousel-query]').dacCarouselQuery();
   4741   });
   4742 })(jQuery);
   4743 
   4744 (function($) {
   4745   /**
   4746    * A CSS based carousel, inspired by SequenceJS.
   4747    * @param {jQuery} el
   4748    * @param {object} options
   4749    * @constructor
   4750    */
   4751   function DacCarousel(el, options) {
   4752     this.el = $(el);
   4753     this.options = options = $.extend({}, DacCarousel.OPTIONS, this.el.data(), options || {});
   4754     this.frames = this.el.find(options.frameSelector);
   4755     this.count = this.frames.size();
   4756     this.current = options.start;
   4757 
   4758     this.initPagination();
   4759     this.initEvents();
   4760     this.initFrame();
   4761   }
   4762 
   4763   DacCarousel.OPTIONS = {
   4764     auto:      true,
   4765     autoTime:  10000,
   4766     autoMinTime: 5000,
   4767     btnPrev:   '[data-carousel-prev]',
   4768     btnNext:   '[data-carousel-next]',
   4769     frameSelector: 'article',
   4770     loop:      true,
   4771     start:     0,
   4772     swipeThreshold: 160,
   4773     pagination: '[data-carousel-pagination]'
   4774   };
   4775 
   4776   DacCarousel.prototype.initPagination = function() {
   4777     this.pagination = $([]);
   4778     if (!this.options.pagination) { return; }
   4779 
   4780     var pagination = $('<ul class="dac-pagination">');
   4781     var parent = this.el;
   4782     if (typeof this.options.pagination === 'string') { parent = this.el.find(this.options.pagination); }
   4783 
   4784     if (this.count > 1) {
   4785       for (var i = 0; i < this.count; i++) {
   4786         var li = $('<li class="dac-pagination-item">').text(i);
   4787         if (i === this.options.start) { li.addClass('active'); }
   4788         li.click(this.go.bind(this, i));
   4789 
   4790         pagination.append(li);
   4791       }
   4792       this.pagination = pagination.children();
   4793       parent.append(pagination);
   4794     }
   4795   };
   4796 
   4797   DacCarousel.prototype.initEvents = function() {
   4798     var that = this;
   4799 
   4800     this.touch = {
   4801       start: {x: 0, y: 0},
   4802       end:   {x: 0, y: 0}
   4803     };
   4804 
   4805     this.el.on('touchstart', this.touchstart_.bind(this));
   4806     this.el.on('touchend', this.touchend_.bind(this));
   4807     this.el.on('touchmove', this.touchmove_.bind(this));
   4808 
   4809     this.el.hover(function() {
   4810       that.pauseRotateTimer();
   4811     }, function() {
   4812       that.startRotateTimer();
   4813     });
   4814 
   4815     $(this.options.btnPrev).click(function(e) {
   4816       e.preventDefault();
   4817       that.prev();
   4818     });
   4819 
   4820     $(this.options.btnNext).click(function(e) {
   4821       e.preventDefault();
   4822       that.next();
   4823     });
   4824   };
   4825 
   4826   DacCarousel.prototype.touchstart_ = function(event) {
   4827     var t = event.originalEvent.touches[0];
   4828     this.touch.start = {x: t.screenX, y: t.screenY};
   4829   };
   4830 
   4831   DacCarousel.prototype.touchend_ = function() {
   4832     var deltaX = this.touch.end.x - this.touch.start.x;
   4833     var deltaY = Math.abs(this.touch.end.y - this.touch.start.y);
   4834     var shouldSwipe = (deltaY < Math.abs(deltaX)) && (Math.abs(deltaX) >= this.options.swipeThreshold);
   4835 
   4836     if (shouldSwipe) {
   4837       if (deltaX > 0) {
   4838         this.prev();
   4839       } else {
   4840         this.next();
   4841       }
   4842     }
   4843   };
   4844 
   4845   DacCarousel.prototype.touchmove_ = function(event) {
   4846     var t = event.originalEvent.touches[0];
   4847     this.touch.end = {x: t.screenX, y: t.screenY};
   4848   };
   4849 
   4850   DacCarousel.prototype.initFrame = function() {
   4851     this.frames.removeClass('active').eq(this.options.start).addClass('active');
   4852   };
   4853 
   4854   DacCarousel.prototype.startRotateTimer = function() {
   4855     if (!this.options.auto || this.rotateTimer) { return; }
   4856     this.rotateTimer = setTimeout(this.next.bind(this), this.options.autoTime);
   4857   };
   4858 
   4859   DacCarousel.prototype.pauseRotateTimer = function() {
   4860     clearTimeout(this.rotateTimer);
   4861     this.rotateTimer = null;
   4862   };
   4863 
   4864   DacCarousel.prototype.prev = function() {
   4865     this.go(this.current - 1);
   4866   };
   4867 
   4868   DacCarousel.prototype.next = function() {
   4869     this.go(this.current + 1);
   4870   };
   4871 
   4872   DacCarousel.prototype.go = function(next) {
   4873     // Figure out what the next slide is.
   4874     while (this.count > 0 && next >= this.count) { next -= this.count; }
   4875     while (next < 0) { next += this.count; }
   4876 
   4877     // Cancel if we're already on that slide.
   4878     if (next === this.current) { return; }
   4879 
   4880     // Prepare next slide.
   4881     this.frames.eq(next).removeClass('out');
   4882 
   4883     // Recalculate styles before starting slide transition.
   4884     this.el.resolveStyles();
   4885     // Update pagination
   4886     this.pagination.removeClass('active').eq(next).addClass('active');
   4887 
   4888     // Transition out current frame
   4889     this.frames.eq(this.current).toggleClass('active out');
   4890 
   4891     // Transition in a new frame
   4892     this.frames.eq(next).toggleClass('active');
   4893 
   4894     this.current = next;
   4895   };
   4896 
   4897   // Helper which resolves new styles for an element, so it can start transitioning
   4898   // from the new values.
   4899   $.fn.resolveStyles = function() {
   4900     /*jshint expr:true*/
   4901     this[0] && this[0].offsetTop;
   4902     return this;
   4903   };
   4904 
   4905   // jQuery plugin
   4906   $.fn.dacCarousel = function() {
   4907     this.each(function() {
   4908       var $el = $(this);
   4909       $el.data('dac-carousel', new DacCarousel(this));
   4910     });
   4911     return this;
   4912   };
   4913 
   4914   // Data API
   4915   $(function() {
   4916     $('[data-carousel]').dacCarousel();
   4917   });
   4918 })(jQuery);
   4919 
   4920 (function($) {
   4921   'use strict';
   4922 
   4923   function Modal(el, options) {
   4924     this.el = $(el);
   4925     this.options = $.extend({}, ToggleModal.DEFAULTS_, options);
   4926     this.isOpen = false;
   4927 
   4928     this.el.on('click', function(event) {
   4929       if (!$.contains($('.dac-modal-window')[0], event.target)) {
   4930         return this.el.trigger('modal-close');
   4931       }
   4932     }.bind(this));
   4933 
   4934     this.el.on('modal-open', this.open_.bind(this));
   4935     this.el.on('modal-close', this.close_.bind(this));
   4936     this.el.on('modal-toggle', this.toggle_.bind(this));
   4937   }
   4938 
   4939   Modal.prototype.toggle_ = function() {
   4940     this.el.trigger('modal-' + (this.isOpen ? 'close' : 'open'));
   4941   };
   4942 
   4943   Modal.prototype.close_ = function() {
   4944     this.el.removeClass('dac-active');
   4945     $('body').removeClass('dac-modal-open');
   4946     this.isOpen = false;
   4947     // When closing the modal for Android Studio downloads, reload the page
   4948     // because otherwise we might get stuck with post-download dialog state
   4949     if ($("[data-modal='studio_tos']").length) {
   4950       location.reload();
   4951     }
   4952   };
   4953 
   4954   Modal.prototype.open_ = function() {
   4955     this.el.addClass('dac-active');
   4956     $('body').addClass('dac-modal-open');
   4957     this.isOpen = true;
   4958   };
   4959 
   4960   function ToggleModal(el, options) {
   4961     this.el = $(el);
   4962     this.options = $.extend({}, ToggleModal.DEFAULTS_, options);
   4963     this.modal = this.options.modalToggle ? $('[data-modal="' + this.options.modalToggle + '"]') :
   4964       this.el.closest('[data-modal]');
   4965 
   4966     this.el.on('click', this.clickHandler_.bind(this));
   4967   }
   4968 
   4969   ToggleModal.prototype.clickHandler_ = function(event) {
   4970     event.preventDefault();
   4971     this.modal.trigger('modal-toggle');
   4972   };
   4973 
   4974   /**
   4975    * jQuery plugin
   4976    * @param  {object} options - Override default options.
   4977    */
   4978   $.fn.dacModal = function(options) {
   4979     return this.each(function() {
   4980       new Modal(this, options);
   4981     });
   4982   };
   4983 
   4984   $.fn.dacToggleModal = function(options) {
   4985     return this.each(function() {
   4986       new ToggleModal(this, options);
   4987     });
   4988   };
   4989 
   4990   /**
   4991    * Data Attribute API
   4992    */
   4993   $(document).on('ready.aranja', function() {
   4994     $('[data-modal]').each(function() {
   4995       $(this).dacModal($(this).data());
   4996     });
   4997 
   4998     $('[data-modal-toggle]').each(function() {
   4999       $(this).dacToggleModal($(this).data());
   5000     });
   5001   });
   5002 })(jQuery);
   5003 
   5004 (function($) {
   5005   'use strict';
   5006 
   5007   /**
   5008    * Toggle the visabilty of the mobile navigation.
   5009    * @param {HTMLElement} el - The DOM element.
   5010    * @param options
   5011    * @constructor
   5012    */
   5013   function ToggleNav(el, options) {
   5014     this.el = $(el);
   5015     this.options = $.extend({}, ToggleNav.DEFAULTS_, options);
   5016     this.options.target = [this.options.navigation];
   5017 
   5018     if (this.options.body) {this.options.target.push('body')}
   5019     if (this.options.dimmer) {this.options.target.push(this.options.dimmer)}
   5020 
   5021     this.el.on('click', this.clickHandler_.bind(this));
   5022   }
   5023 
   5024   /**
   5025    * ToggleNav Default Settings
   5026    * @type {{body: boolean, dimmer: string, navigation: string, toggleClass: string}}
   5027    * @private
   5028    */
   5029   ToggleNav.DEFAULTS_ = {
   5030     body: true,
   5031     dimmer: '.dac-nav-dimmer',
   5032     navigation: '[data-dac-nav]',
   5033     toggleClass: 'dac-nav-open'
   5034   };
   5035 
   5036   /**
   5037    * The actual toggle logic.
   5038    * @param event
   5039    * @private
   5040    */
   5041   ToggleNav.prototype.clickHandler_ = function(event) {
   5042     event.preventDefault();
   5043     $(this.options.target.join(', ')).toggleClass(this.options.toggleClass);
   5044   };
   5045 
   5046   /**
   5047    * jQuery plugin
   5048    * @param  {object} options - Override default options.
   5049    */
   5050   $.fn.dacToggleMobileNav = function(options) {
   5051     return this.each(function() {
   5052       new ToggleNav(this, options);
   5053     });
   5054   };
   5055 
   5056   /**
   5057    * Data Attribute API
   5058    */
   5059   $(window).on('load.aranja', function() {
   5060     $('[data-dac-toggle-nav]').each(function() {
   5061       $(this).dacToggleMobileNav($(this).data());
   5062     });
   5063   });
   5064 })(jQuery);
   5065 
   5066 (function($) {
   5067   'use strict';
   5068 
   5069   /**
   5070    * Submit the newsletter form to a Google Form.
   5071    * @param {HTMLElement} el - The Form DOM element.
   5072    * @constructor
   5073    */
   5074   function NewsletterForm(el) {
   5075     this.el = $(el);
   5076     this.form = this.el.find('form');
   5077     $('<iframe/>').hide()
   5078       .attr('name', 'dac-newsletter-iframe')
   5079       .attr('src', '')
   5080       .insertBefore(this.form);
   5081     this.form.on('submit', this.submitHandler_.bind(this));
   5082   }
   5083 
   5084   /**
   5085    * Milliseconds until modal has vanished after modal-close is triggered.
   5086    * @type {number}
   5087    * @private
   5088    */
   5089   NewsletterForm.CLOSE_DELAY_ = 300;
   5090 
   5091   /**
   5092    * Switch view to display form after close.
   5093    * @private
   5094    */
   5095   NewsletterForm.prototype.closeHandler_ = function() {
   5096     setTimeout(function() {
   5097       this.el.trigger('swap-reset');
   5098     }.bind(this), NewsletterForm.CLOSE_DELAY_);
   5099   };
   5100 
   5101   /**
   5102    * Reset the modal to initial state.
   5103    * @private
   5104    */
   5105   NewsletterForm.prototype.reset_ = function() {
   5106     this.form.trigger('reset');
   5107     this.el.one('modal-close', this.closeHandler_.bind(this));
   5108   };
   5109 
   5110   /**
   5111    * Display a success view on submit.
   5112    * @private
   5113    */
   5114   NewsletterForm.prototype.submitHandler_ = function() {
   5115     this.el.one('swap-complete', this.reset_.bind(this));
   5116     this.el.trigger('swap-content');
   5117   };
   5118 
   5119   /**
   5120    * jQuery plugin
   5121    * @param  {object} options - Override default options.
   5122    */
   5123   $.fn.dacNewsletterForm = function(options) {
   5124     return this.each(function() {
   5125       new NewsletterForm(this, options);
   5126     });
   5127   };
   5128 
   5129   /**
   5130    * Data Attribute API
   5131    */
   5132   $(document).on('ready.aranja', function() {
   5133     $('[data-newsletter]').each(function() {
   5134       $(this).dacNewsletterForm();
   5135     });
   5136   });
   5137 })(jQuery);
   5138 
   5139 (function($) {
   5140   'use strict';
   5141 
   5142   /**
   5143    * Smoothly scroll to location on current page.
   5144    * @param el
   5145    * @param options
   5146    * @constructor
   5147    */
   5148   function ScrollButton(el, options) {
   5149     this.el = $(el);
   5150     this.target = $(this.el.attr('href'));
   5151     this.options = $.extend({}, ScrollButton.DEFAULTS_, options);
   5152 
   5153     if (typeof this.options.offset === 'string') {
   5154       this.options.offset = $(this.options.offset).height();
   5155     }
   5156 
   5157     this.el.on('click', this.clickHandler_.bind(this));
   5158   }
   5159 
   5160   /**
   5161    * Default options
   5162    * @type {{duration: number, easing: string, offset: number, scrollContainer: string}}
   5163    * @private
   5164    */
   5165   ScrollButton.DEFAULTS_ = {
   5166     duration: 300,
   5167     easing: 'swing',
   5168     offset: 0,
   5169     scrollContainer: 'html, body'
   5170   };
   5171 
   5172   /**
   5173    * Scroll logic
   5174    * @param event
   5175    * @private
   5176    */
   5177   ScrollButton.prototype.clickHandler_ = function(event) {
   5178     if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
   5179       return;
   5180     }
   5181 
   5182     event.preventDefault();
   5183 
   5184     $(this.options.scrollContainer).animate({
   5185       scrollTop: this.target.offset().top - this.options.offset
   5186     }, this.options);
   5187   };
   5188 
   5189   /**
   5190    * jQuery plugin
   5191    * @param  {object} options - Override default options.
   5192    */
   5193   $.fn.dacScrollButton = function(options) {
   5194     return this.each(function() {
   5195       new ScrollButton(this, options);
   5196     });
   5197   };
   5198 
   5199   /**
   5200    * Data Attribute API
   5201    */
   5202   $(document).on('ready.aranja', function() {
   5203     $('[data-scroll-button]').each(function() {
   5204       $(this).dacScrollButton($(this).data());
   5205     });
   5206   });
   5207 })(jQuery);
   5208 
   5209 (function($) {
   5210   'use strict';
   5211 
   5212   /**
   5213    * A component that swaps two dynamic height views with an animation.
   5214    * Listens for the following events:
   5215    * * swap-content: triggers SwapContent.swap_()
   5216    * * swap-reset: triggers SwapContent.reset()
   5217    * @param el
   5218    * @param options
   5219    * @constructor
   5220    */
   5221   function SwapContent(el, options) {
   5222     this.el = $(el);
   5223     this.options = $.extend({}, SwapContent.DEFAULTS_, options);
   5224     this.containers = this.el.find(this.options.container);
   5225     this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0);
   5226     this.el.on('swap-content', this.swap.bind(this));
   5227     this.el.on('swap-reset', this.reset.bind(this));
   5228   }
   5229 
   5230   /**
   5231    * SwapContent's default settings.
   5232    * @type {{activeClass: string, container: string, transitionSpeed: number}}
   5233    * @private
   5234    */
   5235   SwapContent.DEFAULTS_ = {
   5236     activeClass: 'dac-active',
   5237     container: '[data-swap-container]',
   5238     transitionSpeed: 500
   5239   };
   5240 
   5241   /**
   5242    * Returns container's visible height.
   5243    * @param container
   5244    * @returns {number}
   5245    */
   5246   SwapContent.prototype.currentHeight = function(container) {
   5247     return container.children('.' + this.options.activeClass).outerHeight();
   5248   };
   5249 
   5250   /**
   5251    * Reset to show initial content
   5252    */
   5253   SwapContent.prototype.reset = function() {
   5254     if (!this.initiallyActive.hasClass(this.initiallyActive)) {
   5255       this.containers.children().toggleClass(this.options.activeClass);
   5256     }
   5257   };
   5258 
   5259   /**
   5260    * Complete the swap.
   5261    */
   5262   SwapContent.prototype.complete = function() {
   5263     this.containers.height('auto');
   5264     this.containers.trigger('swap-complete');
   5265   };
   5266 
   5267   /**
   5268    * Perform the swap of content.
   5269    */
   5270   SwapContent.prototype.swap = function() {
   5271     console.log(this.containers);
   5272     this.containers.each(function(index, container) {
   5273       container = $(container);
   5274       container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass);
   5275       container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed,
   5276         this.complete.bind(this));
   5277     }.bind(this));
   5278   };
   5279 
   5280   /**
   5281    * jQuery plugin
   5282    * @param  {object} options - Override default options.
   5283    */
   5284   $.fn.dacSwapContent = function(options) {
   5285     return this.each(function() {
   5286       new SwapContent(this, options);
   5287     });
   5288   };
   5289 
   5290   /**
   5291    * Data Attribute API
   5292    */
   5293   $(document).on('ready.aranja', function() {
   5294     $('[data-swap]').each(function() {
   5295       $(this).dacSwapContent($(this).data());
   5296     });
   5297   });
   5298 })(jQuery);
   5299 
   5300 (function($) {
   5301   function Toggle(el) {
   5302     $(el).on('click.dac.togglesection', this.toggle);
   5303   }
   5304 
   5305   Toggle.prototype.toggle = function() {
   5306     var $this = $(this);
   5307 
   5308     var $parent = getParent($this);
   5309     var isExpanded = $parent.hasClass('is-expanded');
   5310 
   5311     transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded);
   5312     $parent.toggleClass('is-expanded');
   5313 
   5314     return false;
   5315   };
   5316 
   5317   function getParent($this) {
   5318     var selector = $this.attr('data-target');
   5319 
   5320     if (!selector) {
   5321       selector = $this.attr('href');
   5322       selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
   5323     }
   5324 
   5325     var $parent = selector && $(selector);
   5326 
   5327     $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle');
   5328 
   5329     return $parent.length ? $parent : $this.parent();
   5330   }
   5331 
   5332   /**
   5333    * Runs a transition of max-height along with responsive styles which hide or expand the element.
   5334    * @param $el
   5335    * @param visible
   5336    */
   5337   function transitionMaxHeight($el, visible) {
   5338     var contentHeight = $el.prop('scrollHeight');
   5339     var targetHeight = visible ? contentHeight : 0;
   5340     var duration = $el.transitionDuration();
   5341 
   5342     // If we're hiding, first set the maxHeight we're transitioning from.
   5343     if (!visible) {
   5344       $el.css('maxHeight', contentHeight + 'px')
   5345         .resolveStyles();
   5346     }
   5347 
   5348     // Transition to new state
   5349     $el.css('maxHeight', targetHeight);
   5350 
   5351     // Reset maxHeight to css value after transition.
   5352     setTimeout(function() {
   5353       $el.css('maxHeight', '');
   5354     }, duration);
   5355   }
   5356 
   5357   // Utility to get the transition duration for the element.
   5358   $.fn.transitionDuration = function() {
   5359     var d = $(this).css('transitionDuration') || '0s';
   5360 
   5361     return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0);
   5362   };
   5363 
   5364   // jQuery plugin
   5365   $.fn.toggleSection = function(option) {
   5366     return this.each(function() {
   5367       var $this = $(this);
   5368       var data = $this.data('dac.togglesection');
   5369       if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));}
   5370       if (typeof option === 'string') {data[option].call($this);}
   5371     });
   5372   };
   5373 
   5374   // Data api
   5375   $(document)
   5376     .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle);
   5377 })(jQuery);
   5378