Home | History | Annotate | Download | only in js
      1 var cookie_namespace = 'android_developer';
      2 var isMobile = false; // true if mobile, so we can adjust some layout
      3 var mPagePath; // initialized in ready() function
      4 
      5 var basePath = getBaseUri(location.pathname);
      6 var SITE_ROOT = toRoot + basePath.substring(1, basePath.indexOf("/", 1));
      7 
      8 // TODO(akassay) generate this var in the reference doc build.
      9 var API_LEVELS = ['1', '2', '3', '4', '5', '6', '7', '8', '9',
     10       '10', '11', '12', '13', '14', '15', '16',
     11       '17', '18', '19', '20', '21', '22', '23', '24', '25', 'O'];
     12 var METADATA = METADATA || {};
     13 var RESERVED_METADATA_CATEGORY_NAMES = ['extras', 'carousel', 'collections',
     14                                         'searchHeroCollections'];
     15 
     16 // Ensure that all ajax getScript() requests allow caching
     17 $.ajaxSetup({
     18   cache: true
     19 });
     20 
     21 /******  ON LOAD SET UP STUFF *********/
     22 
     23 $(document).ready(function() {
     24 
     25   // prep nav expandos
     26   var pagePath = location.href.replace(location.hash, '');
     27   // account for intl docs by removing the intl/*/ path
     28   if (pagePath.indexOf("/intl/") == 0) {
     29     pagePath = pagePath.substr(pagePath.indexOf("/", 6)); // start after intl/ to get last /
     30   }
     31 
     32   if (pagePath.indexOf(SITE_ROOT) == 0) {
     33     if (pagePath == '' || pagePath.charAt(pagePath.length - 1) == '/') {
     34       pagePath += 'index.html';
     35     }
     36   }
     37 
     38   // Need a copy of the pagePath before it gets changed in the next block;
     39   // it's needed to perform proper tab highlighting in offline docs (see rootDir below)
     40   var pagePathOriginal = pagePath;
     41   if (SITE_ROOT.match(/\.\.\//) || SITE_ROOT == '') {
     42     // If running locally, SITE_ROOT will be a relative path, so account for that by
     43     // finding the relative URL to this page. This will allow us to find links on the page
     44     // leading back to this page.
     45     var pathParts = pagePath.split('/');
     46     var relativePagePathParts = [];
     47     var upDirs = (SITE_ROOT.match(/(\.\.\/)+/) || [''])[0].length / 3;
     48     for (var i = 0; i < upDirs; i++) {
     49       relativePagePathParts.push('..');
     50     }
     51     for (var i = 0; i < upDirs; i++) {
     52       relativePagePathParts.push(pathParts[pathParts.length - (upDirs - i) - 1]);
     53     }
     54     relativePagePathParts.push(pathParts[pathParts.length - 1]);
     55     pagePath = relativePagePathParts.join('/');
     56   } else {
     57     // Otherwise the page path is already an absolute URL
     58   }
     59 
     60   // set global variable so we can highlight the sidenav a bit later (such as for google reference)
     61   // and highlight the sidenav
     62   mPagePath = pagePath;
     63 
     64   // Check for params and remove them.
     65   mPagePath = mPagePath.split('?')[0];
     66   highlightSidenav();
     67 
     68   // set up prev/next links if they exist
     69   var $selNavLink = $('#nav').find('a[href="' + pagePath + '"]');
     70   var $selListItem;
     71   if ($selNavLink.length) {
     72     $selListItem = $selNavLink.closest('li');
     73 
     74     // set up prev links
     75     var $prevLink = [];
     76     var $prevListItem = $selListItem.prev('li');
     77 
     78     var crossBoundaries = ($("body.design").length > 0) || ($("body.guide").length > 0) ? true :
     79 false; // navigate across topic boundaries only in design docs
     80     if ($prevListItem.length) {
     81       if ($prevListItem.hasClass('nav-section') || crossBoundaries) {
     82         // jump to last topic of previous section
     83         $prevLink = $prevListItem.find('a:last');
     84       } else if (!$selListItem.hasClass('nav-section')) {
     85         // jump to previous topic in this section
     86         $prevLink = $prevListItem.find('a:eq(0)');
     87       }
     88     } else {
     89       // jump to this section's index page (if it exists)
     90       var $parentListItem = $selListItem.parents('li');
     91       $prevLink = $selListItem.parents('li').find('a');
     92 
     93       // except if cross boundaries aren't allowed, and we're at the top of a section already
     94       // (and there's another parent)
     95       if (!crossBoundaries && $parentListItem.hasClass('nav-section') &&
     96                            $selListItem.hasClass('nav-section')) {
     97         $prevLink = [];
     98       }
     99     }
    100 
    101     // set up next links
    102     var $nextLink = [];
    103     var startClass = false;
    104     var isCrossingBoundary = false;
    105 
    106     if ($selListItem.hasClass('nav-section') && $selListItem.children('div.empty').length == 0) {
    107       // we're on an index page, jump to the first topic
    108       $nextLink = $selListItem.find('ul:eq(0)').find('a:eq(0)');
    109 
    110       // if there aren't any children, go to the next section (required for About pages)
    111       if ($nextLink.length == 0) {
    112         $nextLink = $selListItem.next('li').find('a');
    113       } else if ($('.topic-start-link').length) {
    114         // as long as there's a child link and there is a "topic start link" (we're on a landing)
    115         // then set the landing page "start link" text to be the first doc title
    116         $('.topic-start-link').text($nextLink.text().toUpperCase());
    117       }
    118 
    119       // If the selected page has a description, then it's a class or article homepage
    120       if ($selListItem.find('a[description]').length) {
    121         // this means we're on a class landing page
    122         startClass = true;
    123       }
    124     } else {
    125       // jump to the next topic in this section (if it exists)
    126       $nextLink = $selListItem.next('li').find('a:eq(0)');
    127       if ($nextLink.length == 0) {
    128         isCrossingBoundary = true;
    129         // no more topics in this section, jump to the first topic in the next section
    130         $nextLink = $selListItem.parents('li:eq(0)').next('li').find('a:eq(0)');
    131         if (!$nextLink.length) {  // Go up another layer to look for next page (lesson > class > course)
    132           $nextLink = $selListItem.parents('li:eq(1)').next('li.nav-section').find('a:eq(0)');
    133           if ($nextLink.length == 0) {
    134             // if that doesn't work, we're at the end of the list, so disable NEXT link
    135             $('.next-page-link').attr('href', '').addClass("disabled")
    136                                 .click(function() { return false; });
    137             // and completely hide the one in the footer
    138             $('.content-footer .next-page-link').hide();
    139           }
    140         }
    141       }
    142     }
    143 
    144     if (startClass) {
    145       $('.start-class-link').attr('href', $nextLink.attr('href')).removeClass("hide");
    146 
    147       // if there's no training bar (below the start button),
    148       // then we need to add a bottom border to button
    149       if (!$("#tb").length) {
    150         $('.start-class-link').css({'border-bottom':'1px solid #DADADA'});
    151       }
    152     } else if (isCrossingBoundary && !$('body.design').length) {  // Design always crosses boundaries
    153       $('.content-footer.next-class').show();
    154       $('.next-page-link').attr('href', '')
    155                           .removeClass("hide").addClass("disabled")
    156                           .click(function() { return false; });
    157       // and completely hide the one in the footer
    158       $('.content-footer .next-page-link').hide();
    159       $('.content-footer .prev-page-link').hide();
    160 
    161       if ($nextLink.length) {
    162         $('.next-class-link').attr('href', $nextLink.attr('href'))
    163                              .removeClass("hide");
    164 
    165         $('.content-footer .next-class-link').append($nextLink.html());
    166 
    167         $('.next-class-link').find('.new').empty();
    168       }
    169     } else {
    170       $('.next-page-link').attr('href', $nextLink.attr('href'))
    171                           .removeClass("hide");
    172       // for the footer link, also add the previous and next page titles
    173       if ($prevLink.length) {
    174         $('.content-footer .prev-page-link').append($prevLink.html());
    175       }
    176       if ($nextLink.length) {
    177         $('.content-footer .next-page-link').append($nextLink.html());
    178       }
    179     }
    180 
    181     if (!startClass && $prevLink.length) {
    182       var prevHref = $prevLink.attr('href');
    183       if (prevHref == SITE_ROOT + 'index.html') {
    184         // Don't show Previous when it leads to the homepage
    185       } else {
    186         $('.prev-page-link').attr('href', $prevLink.attr('href')).removeClass("hide");
    187       }
    188     }
    189   }
    190 
    191   // Set up the course landing pages for Training with class names and descriptions
    192   if ($('body.trainingcourse').length) {
    193     var $classLinks = $selListItem.find('ul li a').not('#nav .nav-section .nav-section ul a');
    194 
    195     // create an array for all the class descriptions
    196     var $classDescriptions = new Array($classLinks.length);
    197     var lang = getLangPref();
    198     $classLinks.each(function(index) {
    199       var langDescr = $(this).attr(lang + "-description");
    200       if (typeof langDescr !== 'undefined' && langDescr !== false) {
    201         // if there's a class description in the selected language, use that
    202         $classDescriptions[index] = langDescr;
    203       } else {
    204         // otherwise, use the default english description
    205         $classDescriptions[index] = $(this).attr("description");
    206       }
    207     });
    208 
    209     var $olClasses  = $('<ol class="class-list"></ol>');
    210     var $liClass;
    211     var $h2Title;
    212     var $pSummary;
    213     var $olLessons;
    214     var $liLesson;
    215     $classLinks.each(function(index) {
    216       $liClass  = $('<li class="clearfix"></li>');
    217       $h2Title  = $('<a class="title" href="' + $(this).attr('href') + '"><h2 class="norule">' + $(this).html() + '</h2><span></span></a>');
    218       $pSummary = $('<p class="description">' + $classDescriptions[index] + '</p>');
    219 
    220       $olLessons  = $('<ol class="lesson-list"></ol>');
    221 
    222       $lessons = $(this).closest('li').find('ul li a');
    223 
    224       if ($lessons.length) {
    225         $lessons.each(function(index) {
    226           $olLessons.append('<li><a href="' + $(this).attr('href') + '">' + $(this).html() + '</a></li>');
    227         });
    228       } else {
    229         $pSummary.addClass('article');
    230       }
    231 
    232       $liClass.append($h2Title).append($pSummary).append($olLessons);
    233       $olClasses.append($liClass);
    234     });
    235     $('#classes').append($olClasses);
    236   }
    237 
    238   // Set up expand/collapse behavior
    239   initExpandableNavItems("#nav");
    240 
    241   // Set up play-on-hover <video> tags.
    242   $('video.play-on-hover').bind('click', function() {
    243     $(this).get(0).load(); // in case the video isn't seekable
    244     $(this).get(0).play();
    245   });
    246 
    247   // Set up play-on-click for <video> tags with a "video-wrapper".
    248   $('.video-wrapper > video').bind('click', function() {
    249     this.play();
    250     $(this.parentElement).addClass('playing');
    251   });
    252 
    253   // Set up tooltips
    254   var TOOLTIP_MARGIN = 10;
    255   $('acronym,.tooltip-link').each(function() {
    256     var $target = $(this);
    257     var $tooltip = $('<div>')
    258         .addClass('tooltip-box')
    259         .append($target.attr('title'))
    260         .hide()
    261         .appendTo('body');
    262     $target.removeAttr('title');
    263 
    264     $target.hover(function() {
    265       // in
    266       var targetRect = $target.offset();
    267       targetRect.width = $target.width();
    268       targetRect.height = $target.height();
    269 
    270       $tooltip.css({
    271         left: targetRect.left,
    272         top: targetRect.top + targetRect.height + TOOLTIP_MARGIN
    273       });
    274       $tooltip.addClass('below');
    275       $tooltip.show();
    276     }, function() {
    277       // out
    278       $tooltip.hide();
    279     });
    280   });
    281 
    282   // Set up <h2> deeplinks
    283   $('h2').click(function() {
    284     var id = $(this).attr('id');
    285     if (id) {
    286       if (history && history.replaceState) {
    287         // Change url without scrolling.
    288         history.replaceState({}, '', '#' + id);
    289       } else {
    290         document.location.hash = id;
    291       }
    292     }
    293   });
    294 
    295   //Loads the +1 button
    296   //var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
    297   //po.src = 'https://apis.google.com/js/plusone.js';
    298   //var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
    299 });
    300 // END of the onload event
    301 
    302 function initExpandableNavItems(rootTag) {
    303   var toggleIcon = $(
    304       rootTag + ' li.nav-section .nav-section-header .toggle-icon, ' +
    305       rootTag + ' li.nav-section .nav-section-header a[href="#"]');
    306 
    307   toggleIcon.on('click keypress', function(e) {
    308     if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
    309       doNavToggle(this);
    310     }
    311   });
    312 
    313   // Stop expand/collapse behavior when clicking on nav section links
    314   // (since we're navigating away from the page)
    315   // This selector captures the first instance of <a>, but not those with "#" as the href.
    316   $('.nav-section-header').find('a:eq(0)').not('a[href="#"]').click(function(evt) {
    317     window.location.href = $(this).attr('href');
    318     return false;
    319   });
    320 }
    321 
    322 function doNavToggle(el) {
    323   var section = $(el).closest('li.nav-section');
    324   if (section.hasClass('expanded')) {
    325     /* hide me and descendants */
    326     section.find('ul').slideUp(250, function() {
    327       // remove 'expanded' class from my section and any children
    328       section.closest('li').removeClass('expanded');
    329       $('li.nav-section', section).removeClass('expanded');
    330     });
    331   } else {
    332     /* show me */
    333     // first hide all other siblings
    334     var $others = $('li.nav-section.expanded', $(el).closest('ul')).not('.sticky');
    335     $others.removeClass('expanded').children('ul').slideUp(250);
    336 
    337     // now expand me
    338     section.closest('li').addClass('expanded');
    339     section.children('ul').slideDown(250);
    340   }
    341 }
    342 
    343 /** Highlight the current page in sidenav, expanding children as appropriate */
    344 function highlightSidenav() {
    345   // if something is already highlighted, undo it. This is for dynamic navigation (Samples index)
    346   if ($("ul#nav li.selected").length) {
    347     unHighlightSidenav();
    348   }
    349   // look for URL in sidenav, including the hash
    350   var $selNavLink = $('#nav').find('a[href="' + mPagePath + location.hash + '"]');
    351 
    352   // If the selNavLink is still empty, look for it without the hash
    353   if ($selNavLink.length == 0) {
    354     $selNavLink = $('#nav').find('a[href="' + mPagePath + '"]');
    355   }
    356 
    357   var $selListItem;
    358   var breadcrumb = [];
    359 
    360   if ($selNavLink.length) {
    361     // Find this page's <li> in sidenav and set selected
    362     $selListItem = $selNavLink.closest('li');
    363     $selListItem.addClass('selected');
    364 
    365     // Traverse up the tree and expand all parent nav-sections
    366     $selNavLink.parents('li.nav-section').each(function() {
    367       $(this).addClass('expanded');
    368       $(this).children('ul').show();
    369 
    370       var link = $(this).find('a').first();
    371 
    372       if (!$(this).is($selListItem)) {
    373         breadcrumb.unshift(link)
    374       }
    375     });
    376 
    377     $('#nav').scrollIntoView($selNavLink);
    378   }
    379 
    380   breadcrumb.forEach(function(link) {
    381     link.dacCrumbs();
    382   });
    383 }
    384 
    385 function unHighlightSidenav() {
    386   $("ul#nav li.selected").removeClass("selected");
    387   $('ul#nav li.nav-section.expanded').removeClass('expanded').children('ul').hide();
    388 }
    389 
    390 var agent = navigator['userAgent'].toLowerCase();
    391 // If a mobile phone, set flag and do mobile setup
    392 if ((agent.indexOf("mobile") != -1) ||      // android, iphone, ipod
    393     (agent.indexOf("blackberry") != -1) ||
    394     (agent.indexOf("webos") != -1) ||
    395     (agent.indexOf("mini") != -1)) {        // opera mini browsers
    396   isMobile = true;
    397 }
    398 
    399 $(document).ready(function() {
    400   $("pre:not(.no-pretty-print)").addClass("prettyprint");
    401   prettyPrint();
    402 });
    403 
    404 /* Show popup dialogs */
    405 function showDialog(id) {
    406   $dialog = $("#" + id);
    407   $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>');
    408   $dialog.wrapInner('<div/>');
    409   $dialog.removeClass("hide");
    410 }
    411 
    412 /* #########    COOKIES!     ########## */
    413 
    414 function readCookie(cookie) {
    415   var myCookie = cookie_namespace + "_" + cookie + "=";
    416   if (document.cookie) {
    417     var index = document.cookie.indexOf(myCookie);
    418     if (index != -1) {
    419       var valStart = index + myCookie.length;
    420       var valEnd = document.cookie.indexOf(";", valStart);
    421       if (valEnd == -1) {
    422         valEnd = document.cookie.length;
    423       }
    424       var val = document.cookie.substring(valStart, valEnd);
    425       return val;
    426     }
    427   }
    428   return 0;
    429 }
    430 
    431 function writeCookie(cookie, val, section) {
    432   if (val == undefined) return;
    433   section = section == null ? "_" : "_" + section + "_";
    434   var age = 2 * 365 * 24 * 60 * 60; // set max-age to 2 years
    435   var cookieValue = cookie_namespace + section + cookie + "=" + val +
    436                     "; max-age=" + age + "; path=/";
    437   document.cookie = cookieValue;
    438 }
    439 
    440 /* #########     END COOKIES!     ########## */
    441 
    442 /*
    443  * Manages secion card states and nav resize to conclude loading
    444  */
    445 (function() {
    446   $(document).ready(function() {
    447 
    448     // Stack hover states
    449     $('.section-card-menu').each(function(index, el) {
    450       var height = $(el).height();
    451       $(el).css({height:height + 'px', position:'relative'});
    452       var $cardInfo = $(el).find('.card-info');
    453 
    454       $cardInfo.css({position: 'absolute', bottom:'0px', left:'0px', right:'0px', overflow:'visible'});
    455     });
    456 
    457   });
    458 
    459 })();
    460 
    461 /*      MISC LIBRARY FUNCTIONS     */
    462 
    463 function toggle(obj, slide) {
    464   var ul = $("ul:first", obj);
    465   var li = ul.parent();
    466   if (li.hasClass("closed")) {
    467     if (slide) {
    468       ul.slideDown("fast");
    469     } else {
    470       ul.show();
    471     }
    472     li.removeClass("closed");
    473     li.addClass("open");
    474     $(".toggle-img", li).attr("title", "hide pages");
    475   } else {
    476     ul.slideUp("fast");
    477     li.removeClass("open");
    478     li.addClass("closed");
    479     $(".toggle-img", li).attr("title", "show pages");
    480   }
    481 }
    482 
    483 function buildToggleLists() {
    484   $(".toggle-list").each(
    485     function(i) {
    486       $("div:first", this).append("<a class='toggle-img' href='#' title='show pages' onClick='toggle(this.parentNode.parentNode, true); return false;'></a>");
    487       $(this).addClass("closed");
    488     });
    489 }
    490 
    491 function hideNestedItems(list, toggle) {
    492   $list = $(list);
    493   // hide nested lists
    494   if ($list.hasClass('showing')) {
    495     $("li ol", $list).hide('fast');
    496     $list.removeClass('showing');
    497   // show nested lists
    498   } else {
    499     $("li ol", $list).show('fast');
    500     $list.addClass('showing');
    501   }
    502   $(".more,.less", $(toggle)).toggle();
    503 }
    504 
    505 /* Call this to add listeners to a <select> element for Studio/Eclipse/Other docs */
    506 function setupIdeDocToggle() {
    507   $("select.ide").change(function() {
    508     var selected = $(this).find("option:selected").attr("value");
    509     $(".select-ide").hide();
    510     $(".select-ide." + selected).show();
    511 
    512     $("select.ide").val(selected);
    513   });
    514 }
    515 
    516 /* Used to hide and reveal supplemental content, such as long code samples.
    517    See the companion CSS in android-developer-docs.css */
    518 function toggleContent(obj) {
    519   var div = $(obj).closest(".toggle-content");
    520   var toggleMe = $(".toggle-content-toggleme:eq(0)", div);
    521   if (div.hasClass("closed")) { // if it's closed, open it
    522     toggleMe.slideDown();
    523     $(".toggle-content-text:eq(0)", obj).toggle();
    524     div.removeClass("closed").addClass("open");
    525     $(".toggle-content-img:eq(0)", div).attr("title", "hide").attr("src", toRoot +
    526                   "assets/images/styles/disclosure_up.png");
    527   } else { // if it's open, close it
    528     toggleMe.slideUp('fast', function() {  // Wait until the animation is done before closing arrow
    529       $(".toggle-content-text:eq(0)", obj).toggle();
    530       div.removeClass("open").addClass("closed");
    531       div.find(".toggle-content").removeClass("open").addClass("closed")
    532               .find(".toggle-content-toggleme").hide();
    533       $(".toggle-content-img", div).attr("title", "show").attr("src", toRoot +
    534                   "assets/images/styles/disclosure_down.png");
    535     });
    536   }
    537   return false;
    538 }
    539 
    540 /* New version of expandable content */
    541 function toggleExpandable(link, id) {
    542   if ($(id).is(':visible')) {
    543     $(id).slideUp();
    544     $(link).removeClass('expanded');
    545   } else {
    546     $(id).slideDown();
    547     $(link).addClass('expanded');
    548   }
    549 }
    550 
    551 function hideExpandable(ids) {
    552   $(ids).slideUp();
    553   $(ids).prev('h4').find('a.expandable').removeClass('expanded');
    554 }
    555 
    556 /*
    557  *  Slideshow 1.0
    558  *  Used on /index.html and /develop/index.html for carousel
    559  *
    560  *  Sample usage:
    561  *  HTML -
    562  *  <div class="slideshow-container">
    563  *   <a href="" class="slideshow-prev">Prev</a>
    564  *   <a href="" class="slideshow-next">Next</a>
    565  *   <ul>
    566  *       <li class="item"><img src="images/marquee1.jpg"></li>
    567  *       <li class="item"><img src="images/marquee2.jpg"></li>
    568  *       <li class="item"><img src="images/marquee3.jpg"></li>
    569  *       <li class="item"><img src="images/marquee4.jpg"></li>
    570  *   </ul>
    571  *  </div>
    572  *
    573  *   <script type="text/javascript">
    574  *   $('.slideshow-container').dacSlideshow({
    575  *       auto: true,
    576  *       btnPrev: '.slideshow-prev',
    577  *       btnNext: '.slideshow-next'
    578  *   });
    579  *   </script>
    580  *
    581  *  Options:
    582  *  btnPrev:    optional identifier for previous button
    583  *  btnNext:    optional identifier for next button
    584  *  btnPause:   optional identifier for pause button
    585  *  auto:       whether or not to auto-proceed
    586  *  speed:      animation speed
    587  *  autoTime:   time between auto-rotation
    588  *  easing:     easing function for transition
    589  *  start:      item to select by default
    590  *  scroll:     direction to scroll in
    591  *  pagination: whether or not to include dotted pagination
    592  *
    593  */
    594 
    595 (function($) {
    596   $.fn.dacSlideshow = function(o) {
    597 
    598     //Options - see above
    599     o = $.extend({
    600       btnPrev:   null,
    601       btnNext:   null,
    602       btnPause:  null,
    603       auto:      true,
    604       speed:     500,
    605       autoTime:  12000,
    606       easing:    null,
    607       start:     0,
    608       scroll:    1,
    609       pagination: true
    610 
    611     }, o || {});
    612 
    613     //Set up a carousel for each
    614     return this.each(function() {
    615 
    616       var running = false;
    617       var animCss = o.vertical ? "top" : "left";
    618       var sizeCss = o.vertical ? "height" : "width";
    619       var div = $(this);
    620       var ul = $("ul", div);
    621       var tLi = $("li", ul);
    622       var tl = tLi.size();
    623       var timer = null;
    624 
    625       var li = $("li", ul);
    626       var itemLength = li.size();
    627       var curr = o.start;
    628 
    629       li.css({float: o.vertical ? "none" : "left"});
    630       ul.css({margin: "0", padding: "0", position: "relative", "list-style-type": "none", "z-index": "1"});
    631       div.css({position: "relative", "z-index": "2", left: "0px"});
    632 
    633       var liSize = o.vertical ? height(li) : width(li);
    634       var ulSize = liSize * itemLength;
    635       var divSize = liSize;
    636 
    637       li.css({width: li.width(), height: li.height()});
    638       ul.css(sizeCss, ulSize + "px").css(animCss, -(curr * liSize));
    639 
    640       div.css(sizeCss, divSize + "px");
    641 
    642       //Pagination
    643       if (o.pagination) {
    644         var pagination = $("<div class='pagination'></div>");
    645         var pag_ul = $("<ul></ul>");
    646         if (tl > 1) {
    647           for (var i = 0; i < tl; i++) {
    648             var li = $("<li>" + i + "</li>");
    649             pag_ul.append(li);
    650             if (i == o.start) li.addClass('active');
    651             li.click(function() {
    652               go(parseInt($(this).text()));
    653             })
    654           }
    655           pagination.append(pag_ul);
    656           div.append(pagination);
    657         }
    658       }
    659 
    660       //Previous button
    661       if (o.btnPrev)
    662              $(o.btnPrev).click(function(e) {
    663                e.preventDefault();
    664                return go(curr - o.scroll);
    665              });
    666 
    667       //Next button
    668       if (o.btnNext)
    669              $(o.btnNext).click(function(e) {
    670                e.preventDefault();
    671                return go(curr + o.scroll);
    672              });
    673 
    674       //Pause button
    675       if (o.btnPause)
    676              $(o.btnPause).click(function(e) {
    677                e.preventDefault();
    678                if ($(this).hasClass('paused')) {
    679                  startRotateTimer();
    680                } else {
    681                  pauseRotateTimer();
    682                }
    683              });
    684 
    685       //Auto rotation
    686       if (o.auto) startRotateTimer();
    687 
    688       function startRotateTimer() {
    689         clearInterval(timer);
    690         timer = setInterval(function() {
    691           if (curr == tl - 1) {
    692             go(0);
    693           } else {
    694             go(curr + o.scroll);
    695           }
    696         }, o.autoTime);
    697         $(o.btnPause).removeClass('paused');
    698       }
    699 
    700       function pauseRotateTimer() {
    701         clearInterval(timer);
    702         $(o.btnPause).addClass('paused');
    703       }
    704 
    705       //Go to an item
    706       function go(to) {
    707         if (!running) {
    708 
    709           if (to < 0) {
    710             to = itemLength - 1;
    711           } else if (to > itemLength - 1) {
    712             to = 0;
    713           }
    714           curr = to;
    715 
    716           running = true;
    717 
    718           ul.animate(
    719               animCss == "left" ? {left: -(curr * liSize)} : {top: -(curr * liSize)} , o.speed, o.easing,
    720                      function() {
    721                        running = false;
    722                      }
    723                  );
    724 
    725           $(o.btnPrev + "," + o.btnNext).removeClass("disabled");
    726           $((curr - o.scroll < 0 && o.btnPrev)              ||
    727              (curr + o.scroll > itemLength && o.btnNext)              ||
    728              []
    729            ).addClass("disabled");
    730 
    731           var nav_items = $('li', pagination);
    732           nav_items.removeClass('active');
    733           nav_items.eq(to).addClass('active');
    734 
    735         }
    736         if (o.auto) startRotateTimer();
    737         return false;
    738       };
    739     });
    740   };
    741 
    742   function css(el, prop) {
    743     return parseInt($.css(el[0], prop)) || 0;
    744   };
    745   function width(el) {
    746     return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
    747   };
    748   function height(el) {
    749     return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
    750   };
    751 
    752 })(jQuery);
    753 
    754 /*
    755  *  dacSlideshow 1.0
    756  *  Used on develop/index.html for side-sliding tabs
    757  *
    758  *  Sample usage:
    759  *  HTML -
    760  *  <div class="slideshow-container">
    761  *   <a href="" class="slideshow-prev">Prev</a>
    762  *   <a href="" class="slideshow-next">Next</a>
    763  *   <ul>
    764  *       <li class="item"><img src="images/marquee1.jpg"></li>
    765  *       <li class="item"><img src="images/marquee2.jpg"></li>
    766  *       <li class="item"><img src="images/marquee3.jpg"></li>
    767  *       <li class="item"><img src="images/marquee4.jpg"></li>
    768  *   </ul>
    769  *  </div>
    770  *
    771  *   <script type="text/javascript">
    772  *   $('.slideshow-container').dacSlideshow({
    773  *       auto: true,
    774  *       btnPrev: '.slideshow-prev',
    775  *       btnNext: '.slideshow-next'
    776  *   });
    777  *   </script>
    778  *
    779  *  Options:
    780  *  btnPrev:    optional identifier for previous button
    781  *  btnNext:    optional identifier for next button
    782  *  auto:       whether or not to auto-proceed
    783  *  speed:      animation speed
    784  *  autoTime:   time between auto-rotation
    785  *  easing:     easing function for transition
    786  *  start:      item to select by default
    787  *  scroll:     direction to scroll in
    788  *  pagination: whether or not to include dotted pagination
    789  *
    790  */
    791 (function($) {
    792   $.fn.dacTabbedList = function(o) {
    793 
    794     //Options - see above
    795     o = $.extend({
    796       speed : 250,
    797       easing: null,
    798       nav_id: null,
    799       frame_id: null
    800     }, o || {});
    801 
    802     //Set up a carousel for each
    803     return this.each(function() {
    804 
    805       var curr = 0;
    806       var running = false;
    807       var animCss = "margin-left";
    808       var sizeCss = "width";
    809       var div = $(this);
    810 
    811       var nav = $(o.nav_id, div);
    812       var nav_li = $("li", nav);
    813       var nav_size = nav_li.size();
    814       var frame = div.find(o.frame_id);
    815       var content_width = $(frame).find('ul').width();
    816       //Buttons
    817       $(nav_li).click(function(e) {
    818            go($(nav_li).index($(this)));
    819          })
    820 
    821       //Go to an item
    822       function go(to) {
    823         if (!running) {
    824           curr = to;
    825           running = true;
    826 
    827           frame.animate({'margin-left' : -(curr * content_width)}, o.speed, o.easing,
    828                      function() {
    829                        running = false;
    830                      }
    831                  );
    832 
    833           nav_li.removeClass('active');
    834           nav_li.eq(to).addClass('active');
    835 
    836         }
    837         return false;
    838       };
    839     });
    840   };
    841 
    842   function css(el, prop) {
    843     return parseInt($.css(el[0], prop)) || 0;
    844   };
    845   function width(el) {
    846     return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
    847   };
    848   function height(el) {
    849     return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
    850   };
    851 
    852 })(jQuery);
    853 
    854 /* ######################################################## */
    855 /* #################  JAVADOC REFERENCE ################### */
    856 /* ######################################################## */
    857 
    858 
    859 
    860 var API_LEVEL_COOKIE = "api_level";
    861 var minLevel = 1;
    862 var maxLevel = 1;
    863 
    864 function buildApiLevelSelector() {
    865   maxLevel = API_LEVELS.length;
    866   var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE));
    867   userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default
    868 
    869   minLevel = parseInt($("#doc-api-level").attr("class"));
    870   // Handle provisional api levels; the provisional level will always be the highest possible level
    871   // Provisional api levels will also have a length; other stuff that's just missing a level won't,
    872   // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class)
    873   if (isNaN(minLevel) && minLevel.length) {
    874     minLevel = maxLevel;
    875   }
    876   var select = $("#apiLevelSelector").html("").change(changeApiLevel);
    877   for (var i = maxLevel - 1; i >= 0; i--) {
    878     var option = $("<option />").attr("value", "" + API_LEVELS[i]).append("" + API_LEVELS[i]);
    879     //  if (API_LEVELS[i] < minLevel) option.addClass("absent"); // always false for strings (codenames)
    880     select.append(option);
    881   }
    882 
    883   // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true)
    884   var selectedLevelItem = $("#apiLevelSelector option[value='" + userApiLevel + "']").get(0);
    885   selectedLevelItem.setAttribute('selected', true);
    886 }
    887 
    888 function changeApiLevel() {
    889   maxLevel = API_LEVELS.length;
    890   minLevel = parseInt($('#doc-api-level').attr('class'));
    891   var selectedLevel = maxLevel;
    892 
    893   selectedLevel = parseInt($("#apiLevelSelector option:selected").val());
    894   toggleVisisbleApis(selectedLevel, "body");
    895 
    896   writeCookie(API_LEVEL_COOKIE, selectedLevel, null);
    897 
    898   if (selectedLevel < minLevel) {
    899       // Show the API notice dialog, set number values and button event
    900       $('#api-unavailable').trigger('modal-open');
    901       $('#api-unavailable .selected-level').text(selectedLevel);
    902       $('#api-unavailable .api-level').text(minLevel);
    903       $('#api-unavailable button.ok').attr('onclick','$("#apiLevelSelector").val("' + minLevel + '");changeApiLevel();');
    904   }
    905 }
    906 
    907 function toggleVisisbleApis(selectedLevel, context) {
    908   var apis = $(".api", context);
    909   apis.each(function(i) {
    910     var obj = $(this);
    911     var className = obj.attr("class");
    912     var apiLevelIndex = className.lastIndexOf("-") + 1;
    913     var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex);
    914     apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length;
    915     var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex);
    916     if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail
    917       return;
    918     }
    919     apiLevel = parseInt(apiLevel);
    920 
    921     // Handle provisional api levels; if this item's level is the provisional one, set it to the max
    922     var selectedLevelNum = parseInt(selectedLevel)
    923     var apiLevelNum = parseInt(apiLevel);
    924     if (isNaN(apiLevelNum)) {
    925       apiLevelNum = maxLevel;
    926     }
    927 
    928     // Grey things out that aren't available and give a tooltip title
    929     if (apiLevelNum > selectedLevelNum) {
    930       obj.addClass("absent").attr("title", "Requires API Level \"" +
    931             apiLevel + "\" or higher. To reveal, change the target API level " +
    932               "above the left navigation.");
    933     } else obj.removeClass("absent").removeAttr("title");
    934   });
    935 }
    936 
    937 /* #################  SIDENAV TREE VIEW ################### */
    938 /* TODO: eliminate redundancy with non-google functions */
    939 function init_google_navtree(navtree_id, toroot, root_nodes) {
    940   var me = new Object();
    941   me.toroot = toroot;
    942   me.node = new Object();
    943 
    944   me.node.li = document.getElementById(navtree_id);
    945   if (!me.node.li) {
    946     return;
    947   }
    948 
    949   me.node.children_data = root_nodes;
    950   me.node.children = new Array();
    951   me.node.children_ul = document.createElement("ul");
    952   me.node.get_children_ul = function() { return me.node.children_ul; };
    953   //me.node.children_ul.className = "children_ul";
    954   me.node.li.appendChild(me.node.children_ul);
    955   me.node.depth = 0;
    956 
    957   get_google_node(me, me.node);
    958 }
    959 
    960 function new_google_node(me, mom, text, link, children_data, api_level) {
    961   var node = new Object();
    962   var child;
    963   node.children = Array();
    964   node.children_data = children_data;
    965   node.depth = mom.depth + 1;
    966   node.get_children_ul = function() {
    967       if (!node.children_ul) {
    968         node.children_ul = document.createElement("ul");
    969         node.children_ul.className = "tree-list-children";
    970         node.li.appendChild(node.children_ul);
    971       }
    972       return node.children_ul;
    973     };
    974   node.li = document.createElement("li");
    975 
    976   mom.get_children_ul().appendChild(node.li);
    977 
    978   if (link) {
    979     child = document.createElement("a");
    980 
    981   } else {
    982     child = document.createElement("span");
    983     child.className = "tree-list-subtitle";
    984 
    985   }
    986   if (children_data != null) {
    987     node.li.className = "nav-section";
    988     node.label_div = document.createElement("div");
    989     node.label_div.className = "nav-section-header-ref";
    990     node.li.appendChild(node.label_div);
    991     get_google_node(me, node);
    992     node.label_div.appendChild(child);
    993   } else {
    994     node.li.appendChild(child);
    995   }
    996   if (link) {
    997     child.href = me.toroot + link;
    998   }
    999   node.label = document.createTextNode(text);
   1000   child.appendChild(node.label);
   1001 
   1002   node.children_ul = null;
   1003 
   1004   return node;
   1005 }
   1006 
   1007 function get_google_node(me, mom) {
   1008   mom.children_visited = true;
   1009   var linkText;
   1010   for (var i in mom.children_data) {
   1011     var node_data = mom.children_data[i];
   1012     linkText = node_data[0];
   1013 
   1014     if (linkText.match("^" + "com.google.android") == "com.google.android") {
   1015       linkText = linkText.substr(19, linkText.length);
   1016     }
   1017     mom.children[i] = new_google_node(me, mom, linkText, node_data[1],
   1018         node_data[2], node_data[3]);
   1019   }
   1020 }
   1021 
   1022 /****** NEW version of script to build google and sample navs dynamically ******/
   1023 // TODO: update Google reference docs to tolerate this new implementation
   1024 
   1025 var NODE_NAME = 0;
   1026 var NODE_HREF = 1;
   1027 var NODE_GROUP = 2;
   1028 var NODE_TAGS = 3;
   1029 var NODE_CHILDREN = 4;
   1030 
   1031 function init_google_navtree2(navtree_id, data) {
   1032   var $containerUl = $("#" + navtree_id);
   1033   for (var i in data) {
   1034     var node_data = data[i];
   1035     $containerUl.append(new_google_node2(node_data));
   1036   }
   1037 
   1038   // Make all third-generation list items 'sticky' to prevent them from collapsing
   1039   $containerUl.find('li li li.nav-section').addClass('sticky');
   1040 
   1041   initExpandableNavItems("#" + navtree_id);
   1042 }
   1043 
   1044 function new_google_node2(node_data) {
   1045   var linkText = node_data[NODE_NAME];
   1046   if (linkText.match("^" + "com.google.android") == "com.google.android") {
   1047     linkText = linkText.substr(19, linkText.length);
   1048   }
   1049   var $li = $('<li>');
   1050   var $a;
   1051   if (node_data[NODE_HREF] != null) {
   1052     $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >' +
   1053         linkText + '</a>');
   1054   } else {
   1055     $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >' +
   1056         linkText + '/</a>');
   1057   }
   1058   var $childUl = $('<ul>');
   1059   if (node_data[NODE_CHILDREN] != null) {
   1060     $li.addClass("nav-section");
   1061     $a = $('<div class="nav-section-header">').append($a);
   1062     if (node_data[NODE_HREF] == null) $a.addClass('empty');
   1063 
   1064     for (var i in node_data[NODE_CHILDREN]) {
   1065       var child_node_data = node_data[NODE_CHILDREN][i];
   1066       $childUl.append(new_google_node2(child_node_data));
   1067     }
   1068     $li.append($childUl);
   1069   }
   1070   $li.prepend($a);
   1071 
   1072   return $li;
   1073 }
   1074 
   1075 function showGoogleRefTree() {
   1076   init_default_google_navtree(toRoot);
   1077   init_default_gcm_navtree(toRoot);
   1078 }
   1079 
   1080 function init_default_google_navtree(toroot) {
   1081   // load json file for navtree data
   1082   $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) {
   1083     // when the file is loaded, initialize the tree
   1084     if (jqxhr.status === 200) {
   1085       init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA);
   1086       highlightSidenav();
   1087     }
   1088   });
   1089 }
   1090 
   1091 function init_default_gcm_navtree(toroot) {
   1092   // load json file for navtree data
   1093   $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) {
   1094     // when the file is loaded, initialize the tree
   1095     if (jqxhr.status === 200) {
   1096       init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA);
   1097       highlightSidenav();
   1098     }
   1099   });
   1100 }
   1101 
   1102 /* TOGGLE INHERITED MEMBERS */
   1103 
   1104 /* Toggle an inherited class (arrow toggle)
   1105  * @param linkObj  The link that was clicked.
   1106  * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
   1107  *                'null' to simply toggle.
   1108  */
   1109 function toggleInherited(linkObj, expand) {
   1110   var base = linkObj.getAttribute("id");
   1111   var list = document.getElementById(base + "-list");
   1112   var summary = document.getElementById(base + "-summary");
   1113   var trigger = document.getElementById(base + "-trigger");
   1114   var a = $(linkObj);
   1115   if ((expand == null && a.hasClass("closed")) || expand) {
   1116     list.style.display = "none";
   1117     summary.style.display = "block";
   1118     trigger.src = toRoot + "assets/images/styles/disclosure_up.png";
   1119     a.removeClass("closed");
   1120     a.addClass("opened");
   1121   } else if ((expand == null && a.hasClass("opened")) || (expand == false)) {
   1122     list.style.display = "block";
   1123     summary.style.display = "none";
   1124     trigger.src = toRoot + "assets/images/styles/disclosure_down.png";
   1125     a.removeClass("opened");
   1126     a.addClass("closed");
   1127   }
   1128   return false;
   1129 }
   1130 
   1131 /* Toggle all inherited classes in a single table (e.g. all inherited methods)
   1132  * @param linkObj  The link that was clicked.
   1133  * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
   1134  *                'null' to simply toggle.
   1135  */
   1136 function toggleAllInherited(linkObj, expand) {
   1137   var a = $(linkObj);
   1138   var table = $(a.parent().parent().parent()); // ugly way to get table/tbody
   1139   var expandos = $(".jd-expando-trigger", table);
   1140   if ((expand == null && a.text() == "[Expand]") || expand) {
   1141     expandos.each(function(i) {
   1142       toggleInherited(this, true);
   1143     });
   1144     a.text("[Collapse]");
   1145   } else if ((expand == null && a.text() == "[Collapse]") || (expand == false)) {
   1146     expandos.each(function(i) {
   1147       toggleInherited(this, false);
   1148     });
   1149     a.text("[Expand]");
   1150   }
   1151   return false;
   1152 }
   1153 
   1154 /* Toggle all inherited members in the class (link in the class title)
   1155  */
   1156 function toggleAllClassInherited() {
   1157   var a = $("#toggleAllClassInherited"); // get toggle link from class title
   1158   var toggles = $(".toggle-all", $("#body-content"));
   1159   if (a.text() == "[Expand All]") {
   1160     toggles.each(function(i) {
   1161       toggleAllInherited(this, true);
   1162     });
   1163     a.text("[Collapse All]");
   1164   } else {
   1165     toggles.each(function(i) {
   1166       toggleAllInherited(this, false);
   1167     });
   1168     a.text("[Expand All]");
   1169   }
   1170   return false;
   1171 }
   1172 
   1173 /* Expand all inherited members in the class. Used when initiating page search */
   1174 function ensureAllInheritedExpanded() {
   1175   var toggles = $(".toggle-all", $("#body-content"));
   1176   toggles.each(function(i) {
   1177     toggleAllInherited(this, true);
   1178   });
   1179   $("#toggleAllClassInherited").text("[Collapse All]");
   1180 }
   1181 
   1182 /* HANDLE KEY EVENTS
   1183  * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search)
   1184  */
   1185 var agent = navigator['userAgent'].toLowerCase();
   1186 var mac = agent.indexOf("macintosh") != -1;
   1187 
   1188 $(document).keydown(function(e) {
   1189   var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key
   1190   if (control && e.which == 70) {  // 70 is "F"
   1191     ensureAllInheritedExpanded();
   1192   }
   1193 });
   1194 
   1195 /* On-demand functions */
   1196 
   1197 /** Move sample code line numbers out of PRE block and into non-copyable column */
   1198 function initCodeLineNumbers() {
   1199   var numbers = $("#codesample-block a.number");
   1200   if (numbers.length) {
   1201     $("#codesample-line-numbers").removeClass("hidden").append(numbers);
   1202   }
   1203 
   1204   $(document).ready(function() {
   1205     // select entire line when clicked
   1206     $("span.code-line").click(function() {
   1207       if (!shifted) {
   1208         selectText(this);
   1209       }
   1210     });
   1211     // invoke line link on double click
   1212     $(".code-line").dblclick(function() {
   1213       document.location.hash = $(this).attr('id');
   1214     });
   1215     // highlight the line when hovering on the number
   1216     $("#codesample-line-numbers a.number").mouseover(function() {
   1217       var id = $(this).attr('href');
   1218       $(id).css('background', '#e7e7e7');
   1219     });
   1220     $("#codesample-line-numbers a.number").mouseout(function() {
   1221       var id = $(this).attr('href');
   1222       $(id).css('background', 'none');
   1223     });
   1224   });
   1225 }
   1226 
   1227 // create SHIFT key binder to avoid the selectText method when selecting multiple lines
   1228 var shifted = false;
   1229 $(document).bind('keyup keydown', function(e) {
   1230   shifted = e.shiftKey; return true;
   1231 });
   1232 
   1233 // courtesy of jasonedelman.com
   1234 function selectText(element) {
   1235   var doc = document      ,
   1236         range, selection
   1237   ;
   1238   if (doc.body.createTextRange) { //ms
   1239     range = doc.body.createTextRange();
   1240     range.moveToElementText(element);
   1241     range.select();
   1242   } else if (window.getSelection) { //all others
   1243     selection = window.getSelection();
   1244     range = doc.createRange();
   1245     range.selectNodeContents(element);
   1246     selection.removeAllRanges();
   1247     selection.addRange(range);
   1248   }
   1249 }
   1250 
   1251 /** Display links and other information about samples that match the
   1252     group specified by the URL */
   1253 function showSamples() {
   1254   var group = $("#samples").attr('class');
   1255   $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>");
   1256 
   1257   var $ul = $("<ul>");
   1258   $selectedLi = $("#nav li.selected");
   1259 
   1260   $selectedLi.children("ul").children("li").each(function() {
   1261     var $li = $("<li>").append($(this).find("a").first().clone());
   1262     var $samplesLink = $li.find("a");
   1263     if ($samplesLink.text().endsWith('/')) {
   1264       $samplesLink.text($samplesLink.text().slice(0,-1));
   1265     }
   1266     $ul.append($li);
   1267   });
   1268 
   1269   $("#samples").append($ul);
   1270 
   1271 }
   1272 
   1273 /* ########################################################## */
   1274 /* ###################  RESOURCE CARDS  ##################### */
   1275 /* ########################################################## */
   1276 
   1277 /** Handle resource queries, collections, and grids (sections). Requires
   1278     jd_tag_helpers.js and the *_unified_data.js to be loaded. */
   1279 
   1280 (function() {
   1281   $(document).ready(function() {
   1282     // Need to initialize hero carousel before other sections for dedupe
   1283     // to work correctly.
   1284     $('[data-carousel-query]').dacCarouselQuery();
   1285 
   1286     // Iterate over all instances and initialize a resource widget.
   1287     $('.resource-widget').resourceWidget();
   1288   });
   1289 
   1290   $.fn.widgetOptions = function() {
   1291     return {
   1292       cardSizes: (this.data('cardsizes') || '').split(','),
   1293       maxResults: parseInt(this.data('maxresults'), 10) || Infinity,
   1294       initialResults: this.data('initialResults'),
   1295       itemsPerPage: this.data('itemsPerPage'),
   1296       sortOrder: this.data('sortorder'),
   1297       query: this.data('query'),
   1298       section: this.data('section'),
   1299       /* Added by LFL 6/6/14 */
   1300       resourceStyle: this.data('resourcestyle') || 'card',
   1301       stackSort: this.data('stacksort') || 'true',
   1302       // For filter based resources
   1303       allowDuplicates: this.data('allow-duplicates') || 'false'
   1304     };
   1305   };
   1306 
   1307   $.fn.deprecateOldGridStyles = function() {
   1308     var m = this.get(0).className.match(/\bcol-(\d+)\b/);
   1309     if (m && !this.is('.cols > *')) {
   1310       this.removeClass('col-' + m[1]);
   1311     }
   1312     return this;
   1313   }
   1314 
   1315   /*
   1316    * Three types of resource layouts:
   1317    * Flow - Uses a fixed row-height flow using float left style.
   1318    * Carousel - Single card slideshow all same dimension absolute.
   1319    * Stack - Uses fixed columns and flexible element height.
   1320    */
   1321   function initResourceWidget(widget, resources, opts) {
   1322     var $widget = $(widget).deprecateOldGridStyles();
   1323     var isFlow = $widget.hasClass('resource-flow-layout');
   1324     var isCarousel = $widget.hasClass('resource-carousel-layout');
   1325     var isStack = $widget.hasClass('resource-stack-layout');
   1326 
   1327     opts = opts || $widget.widgetOptions();
   1328     resources = resources || metadata.query(opts);
   1329 
   1330     if (opts.maxResults !== undefined) {
   1331       resources = resources.slice(0, opts.maxResults);
   1332     }
   1333 
   1334     if (isFlow) {
   1335       drawResourcesFlowWidget($widget, opts, resources);
   1336     } else if (isCarousel) {
   1337       drawResourcesCarouselWidget($widget, opts, resources);
   1338     } else if (isStack) {
   1339       opts.numStacks = $widget.data('numstacks');
   1340       drawResourcesStackWidget($widget, opts, resources);
   1341     }
   1342   }
   1343 
   1344   $.fn.resourceWidget = function(resources, options) {
   1345     return this.each(function() {
   1346       initResourceWidget(this, resources, options);
   1347     });
   1348   };
   1349 
   1350   /* Initializes a Resource Carousel Widget */
   1351   function drawResourcesCarouselWidget($widget, opts, resources) {
   1352     $widget.empty();
   1353     var plusone = false; // stop showing plusone buttons on cards
   1354 
   1355     $widget.addClass('resource-card slideshow-container')
   1356       .append($('<a>').addClass('slideshow-prev').text('Prev'))
   1357       .append($('<a>').addClass('slideshow-next').text('Next'));
   1358 
   1359     var css = {'width': $widget.width() + 'px',
   1360                 'height': $widget.height() + 'px'};
   1361 
   1362     var $ul = $('<ul>');
   1363 
   1364     for (var i = 0; i < resources.length; ++i) {
   1365       var $card = $('<a>')
   1366         .attr('href', cleanUrl(resources[i].url))
   1367         .decorateResourceCard(resources[i], plusone);
   1368 
   1369       $('<li>').css(css)
   1370           .append($card)
   1371           .appendTo($ul);
   1372     }
   1373 
   1374     $('<div>').addClass('frame')
   1375       .append($ul)
   1376       .appendTo($widget);
   1377 
   1378     $widget.dacSlideshow({
   1379       auto: true,
   1380       btnPrev: '.slideshow-prev',
   1381       btnNext: '.slideshow-next'
   1382     });
   1383   }
   1384 
   1385   /* Initializes a Resource Card Stack Widget (column-based layout)
   1386      Modified by LFL 6/6/14
   1387    */
   1388   function drawResourcesStackWidget($widget, opts, resources, sections) {
   1389     // Don't empty widget, grab all items inside since they will be the first
   1390     // items stacked, followed by the resource query
   1391     var plusone = false; // stop showing plusone buttons on cards
   1392     var cards = $widget.find('.resource-card').detach().toArray();
   1393     var numStacks = opts.numStacks || 1;
   1394     var $stacks = [];
   1395 
   1396     for (var i = 0; i < numStacks; ++i) {
   1397       $stacks[i] = $('<div>').addClass('resource-card-stack')
   1398           .appendTo($widget);
   1399     }
   1400 
   1401     var sectionResources = [];
   1402 
   1403     // Extract any subsections that are actually resource cards
   1404     if (sections) {
   1405       for (i = 0; i < sections.length; ++i) {
   1406         if (!sections[i].sections || !sections[i].sections.length) {
   1407           // Render it as a resource card
   1408           sectionResources.push(
   1409             $('<a>')
   1410               .addClass('resource-card section-card')
   1411               .attr('href', cleanUrl(sections[i].resource.url))
   1412               .decorateResourceCard(sections[i].resource, plusone)[0]
   1413           );
   1414 
   1415         } else {
   1416           cards.push(
   1417             $('<div>')
   1418               .addClass('resource-card section-card-menu')
   1419               .decorateResourceSection(sections[i], plusone)[0]
   1420           );
   1421         }
   1422       }
   1423     }
   1424 
   1425     cards = cards.concat(sectionResources);
   1426 
   1427     for (i = 0; i < resources.length; ++i) {
   1428       var $card = createResourceElement(resources[i], opts);
   1429 
   1430       if (opts.resourceStyle.indexOf('related') > -1) {
   1431         $card.addClass('related-card');
   1432       }
   1433 
   1434       cards.push($card[0]);
   1435     }
   1436 
   1437     if (opts.stackSort !== 'false') {
   1438       for (i = 0; i < cards.length; ++i) {
   1439         // Find the stack with the shortest height, but give preference to
   1440         // left to right order.
   1441         var minHeight = $stacks[0].height();
   1442         var minIndex = 0;
   1443 
   1444         for (var j = 1; j < numStacks; ++j) {
   1445           var height = $stacks[j].height();
   1446           if (height < minHeight - 45) {
   1447             minHeight = height;
   1448             minIndex = j;
   1449           }
   1450         }
   1451 
   1452         $stacks[minIndex].append($(cards[i]));
   1453       }
   1454     }
   1455   }
   1456 
   1457   /*
   1458     Create a resource card using the given resource object and a list of html
   1459      configured options. Returns a jquery object containing the element.
   1460   */
   1461   function createResourceElement(resource, opts, plusone) {
   1462     var $el;
   1463 
   1464     // The difference here is that generic cards are not entirely clickable
   1465     // so its a div instead of an a tag, also the generic one is not given
   1466     // the resource-card class so it appears with a transparent background
   1467     // and can be styled in whatever way the css setup.
   1468     if (opts.resourceStyle === 'generic') {
   1469       $el = $('<div>')
   1470         .addClass('resource')
   1471         .attr('href', cleanUrl(resource.url))
   1472         .decorateResource(resource, opts);
   1473     } else {
   1474       var cls = 'resource resource-card';
   1475 
   1476       $el = $('<a>')
   1477         .addClass(cls)
   1478         .attr('href', cleanUrl(resource.url))
   1479         .decorateResourceCard(resource, plusone);
   1480     }
   1481 
   1482     return $el;
   1483   }
   1484 
   1485   function createResponsiveFlowColumn(cardSize) {
   1486     var cardWidth = parseInt(cardSize.match(/(\d+)/)[1], 10);
   1487     var column = $('<div>').addClass('col-' + (cardWidth / 3) + 'of6');
   1488     if (cardWidth < 9) {
   1489       column.addClass('col-tablet-1of2');
   1490     } else if (cardWidth > 9 && cardWidth < 18) {
   1491       column.addClass('col-tablet-1of1');
   1492     }
   1493     if (cardWidth < 18) {
   1494       column.addClass('col-mobile-1of1');
   1495     }
   1496     return column;
   1497   }
   1498 
   1499   /* Initializes a flow widget, see distribute.scss for generating accompanying css */
   1500   function drawResourcesFlowWidget($widget, opts, resources) {
   1501     // We'll be doing our own modifications to opts.
   1502     opts = $.extend({}, opts);
   1503 
   1504     $widget.empty().addClass('cols');
   1505     if (opts.itemsPerPage) {
   1506       $('<div class="col-1of1 dac-section-links dac-text-center">')
   1507         .append(
   1508           $('<div class="dac-section-link dac-show-less" data-toggle="show-less">Less<i class="dac-sprite dac-auto-unfold-less"></i></div>'),
   1509           $('<div class="dac-section-link dac-show-more" data-toggle="show-more">More<i class="dac-sprite dac-auto-unfold-more"></i></div>')
   1510         )
   1511         .appendTo($widget);
   1512     }
   1513 
   1514     $widget.data('options.resourceflow', opts);
   1515     $widget.data('resources.resourceflow', resources);
   1516 
   1517     drawResourceFlowPage($widget, opts, resources);
   1518   }
   1519 
   1520   function drawResourceFlowPage($widget, opts, resources) {
   1521     var cardSizes = opts.cardSizes || ['6x6']; // 2015-08-09: dynamic card sizes are deprecated
   1522     var i = opts.currentIndex || 0;
   1523     var j = 0;
   1524     var plusone = false; // stop showing plusone buttons on cards
   1525     var firstPage = i === 0;
   1526     var initialResults = opts.initialResults || opts.itemsPerPage || resources.length;
   1527     var max = firstPage ? initialResults : i + opts.itemsPerPage;
   1528     max = Math.min(resources.length, max);
   1529 
   1530     var page = $('<div class="resource-flow-page">');
   1531     if (opts.itemsPerPage) {
   1532       $widget.find('.dac-section-links').before(page);
   1533     } else {
   1534       $widget.append(page);
   1535     }
   1536 
   1537     while (i < max) {
   1538       var cardSize = cardSizes[j++ % cardSizes.length];
   1539       cardSize = cardSize.replace(/^\s+|\s+$/, '');
   1540 
   1541       var column = createResponsiveFlowColumn(cardSize).appendTo(page);
   1542 
   1543       // A stack has a third dimension which is the number of stacked items
   1544       var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/);
   1545       var stackCount = 0;
   1546       var $stackDiv = null;
   1547 
   1548       if (isStack) {
   1549         // Create a stack container which should have the dimensions defined
   1550         // by the product of the items inside.
   1551         $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1] +
   1552           'x' + isStack[2] * isStack[3]) .appendTo(column);
   1553       }
   1554 
   1555       // Build each stack item or just a single item
   1556       do {
   1557         var resource = resources[i];
   1558 
   1559         var $card = createResourceElement(resources[i], opts, plusone);
   1560 
   1561         $card.addClass('resource-card-' + cardSize +
   1562           ' resource-card-' + resource.type.toLowerCase());
   1563 
   1564         if (isStack) {
   1565           $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]);
   1566           if (++stackCount === parseInt(isStack[3])) {
   1567             $card.addClass('resource-card-row-stack-last');
   1568             stackCount = 0;
   1569           }
   1570         } else {
   1571           stackCount = 0;
   1572         }
   1573 
   1574         $card.appendTo($stackDiv || column);
   1575 
   1576       } while (++i < max && stackCount > 0);
   1577 
   1578       // Record number of pages viewed in analytics.
   1579       if (!firstPage) {
   1580         var clicks = Math.ceil((i - initialResults) / opts.itemsPerPage);
   1581         devsite.analytics.trackAnalyticsEvent('event',
   1582             'Cards', 'Click More', clicks);
   1583       }
   1584     }
   1585 
   1586     opts.currentIndex = i;
   1587     $widget.toggleClass('dac-has-more', i < resources.length);
   1588     $widget.toggleClass('dac-has-less', !firstPage);
   1589 
   1590     $widget.trigger('dac:domchange');
   1591     if (opts.onRenderPage) {
   1592       opts.onRenderPage(page);
   1593     }
   1594   }
   1595 
   1596   function drawResourceFlowReset($widget, opts, resources) {
   1597     $widget.find('.resource-flow-page')
   1598         .slice(1)
   1599         .remove();
   1600     $widget.toggleClass('dac-has-more', true);
   1601     $widget.toggleClass('dac-has-less', false);
   1602 
   1603     opts.currentIndex = Math.min(opts.initialResults, resources.length);
   1604     devsite.analytics.trackAnalyticsEvent('event', 'Cards', 'Click Less');
   1605   }
   1606 
   1607   /* A decorator for event functions which finds the surrounding widget and it's options */
   1608   function wrapWithWidget(func) {
   1609     return function(e) {
   1610       if (e) e.preventDefault();
   1611 
   1612       var $widget = $(this).closest('.resource-flow-layout');
   1613       var opts = $widget.data('options.resourceflow');
   1614       var resources = $widget.data('resources.resourceflow');
   1615       func($widget, opts, resources);
   1616     };
   1617   }
   1618 
   1619   /* Build a site map of resources using a section as a root. */
   1620   function buildSectionList(opts) {
   1621     if (opts.section && SECTION_BY_ID[opts.section]) {
   1622       return SECTION_BY_ID[opts.section].sections || [];
   1623     }
   1624     return [];
   1625   }
   1626 
   1627   function cleanUrl(url) {
   1628     if (url && url.indexOf('//') === -1) {
   1629       url = toRoot + url;
   1630     }
   1631 
   1632     return url;
   1633   }
   1634 
   1635   // Delegated events for resources.
   1636   $(document).on('click', '.resource-flow-layout [data-toggle="show-more"]', wrapWithWidget(drawResourceFlowPage));
   1637   $(document).on('click', '.resource-flow-layout [data-toggle="show-less"]', wrapWithWidget(drawResourceFlowReset));
   1638 })();
   1639 
   1640 (function($) {
   1641   // A mapping from category and type values to new values or human presentable strings.
   1642   var SECTION_MAP = {
   1643     googleplay: 'google play'
   1644   };
   1645 
   1646   /*
   1647     Utility method for creating dom for the description area of a card.
   1648     Used in decorateResourceCard and decorateResource.
   1649   */
   1650   function buildResourceCardDescription(resource, plusone) {
   1651     var $description = $('<div>').addClass('description ellipsis');
   1652 
   1653     $description.append($('<div>').addClass('text').html(resource.summary));
   1654 
   1655     if (resource.cta) {
   1656       $description.append($('<a>').addClass('cta').html(resource.cta));
   1657     }
   1658 
   1659     if (plusone) {
   1660       var plusurl = resource.url.indexOf("//") > -1 ? resource.url :
   1661         "//developer.android.com/" + resource.url;
   1662 
   1663       $description.append($('<div>').addClass('util')
   1664         .append($('<div>').addClass('g-plusone')
   1665           .attr('data-size', 'small')
   1666           .attr('data-align', 'right')
   1667           .attr('data-href', plusurl)));
   1668     }
   1669 
   1670     return $description;
   1671   }
   1672 
   1673   /* Simple jquery function to create dom for a standard resource card */
   1674   $.fn.decorateResourceCard = function(resource, plusone) {
   1675     var section = resource.category || resource.type;
   1676     section = (SECTION_MAP[section] || section).toLowerCase();
   1677     var imgUrl = resource.image ||
   1678       'assets/images/resource-card-default-android.jpg';
   1679 
   1680     if (imgUrl.indexOf('//') === -1) {
   1681       imgUrl = toRoot + imgUrl;
   1682     }
   1683 
   1684     if (resource.type === 'youtube' || resource.type === 'video') {
   1685       $('<div>').addClass('play-button')
   1686         .append($('<i class="dac-sprite dac-play-white">'))
   1687         .appendTo(this);
   1688     }
   1689 
   1690     $('<div>').addClass('card-bg')
   1691       .css('background-image', 'url(' + (imgUrl || toRoot +
   1692         'assets/images/resource-card-default-android.jpg') + ')')
   1693       .appendTo(this);
   1694 
   1695     $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : ''))
   1696       .append($('<div>').addClass('section').text(section))
   1697       .append($('<div>').addClass('title' + (resource.title_highlighted ? ' highlighted' : ''))
   1698         .html(resource.title_highlighted || resource.title))
   1699       .append(buildResourceCardDescription(resource, plusone))
   1700       .appendTo(this);
   1701 
   1702     return this;
   1703   };
   1704 
   1705   /* Simple jquery function to create dom for a resource section card (menu) */
   1706   $.fn.decorateResourceSection = function(section, plusone) {
   1707     var resource = section.resource;
   1708     //keep url clean for matching and offline mode handling
   1709     var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot;
   1710     var $base = $('<a>')
   1711         .addClass('card-bg')
   1712         .attr('href', resource.url)
   1713         .append($('<div>').addClass('card-section-icon')
   1714           .append($('<div>').addClass('icon'))
   1715           .append($('<div>').addClass('section').html(resource.title)))
   1716       .appendTo(this);
   1717 
   1718     var $cardInfo = $('<div>').addClass('card-info').appendTo(this);
   1719 
   1720     if (section.sections && section.sections.length) {
   1721       // Recurse the section sub-tree to find a resource image.
   1722       var stack = [section];
   1723 
   1724       while (stack.length) {
   1725         if (stack[0].resource.image) {
   1726           $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')');
   1727           break;
   1728         }
   1729 
   1730         if (stack[0].sections) {
   1731           stack = stack.concat(stack[0].sections);
   1732         }
   1733 
   1734         stack.shift();
   1735       }
   1736 
   1737       var $ul = $('<ul>')
   1738         .appendTo($cardInfo);
   1739 
   1740       var max = section.sections.length > 3 ? 3 : section.sections.length;
   1741 
   1742       for (var i = 0; i < max; ++i) {
   1743 
   1744         var subResource = section.sections[i];
   1745         if (!plusone) {
   1746           $('<li>')
   1747             .append($('<a>').attr('href', subResource.url)
   1748               .append($('<div>').addClass('title').html(subResource.title))
   1749               .append($('<div>').addClass('description ellipsis')
   1750                 .append($('<div>').addClass('text').html(subResource.summary))
   1751                 .append($('<div>').addClass('util'))))
   1752           .appendTo($ul);
   1753         } else {
   1754           $('<li>')
   1755             .append($('<a>').attr('href', subResource.url)
   1756               .append($('<div>').addClass('title').html(subResource.title))
   1757               .append($('<div>').addClass('description ellipsis')
   1758                 .append($('<div>').addClass('text').html(subResource.summary))
   1759                 .append($('<div>').addClass('util')
   1760                   .append($('<div>').addClass('g-plusone')
   1761                     .attr('data-size', 'small')
   1762                     .attr('data-align', 'right')
   1763                     .attr('data-href', resource.url)))))
   1764           .appendTo($ul);
   1765         }
   1766       }
   1767 
   1768       // Add a more row
   1769       if (max < section.sections.length) {
   1770         $('<li>')
   1771           .append($('<a>').attr('href', resource.url)
   1772             .append($('<div>')
   1773               .addClass('title')
   1774               .text('More')))
   1775         .appendTo($ul);
   1776       }
   1777     } else {
   1778       // No sub-resources, just render description?
   1779     }
   1780 
   1781     return this;
   1782   };
   1783 
   1784   /* Render other types of resource styles that are not cards. */
   1785   $.fn.decorateResource = function(resource, opts) {
   1786     var imgUrl = resource.image ||
   1787       'assets/images/resource-card-default-android.jpg';
   1788     var linkUrl = resource.url;
   1789 
   1790     if (imgUrl.indexOf('//') === -1) {
   1791       imgUrl = toRoot + imgUrl;
   1792     }
   1793 
   1794     if (linkUrl && linkUrl.indexOf('//') === -1) {
   1795       linkUrl = toRoot + linkUrl;
   1796     }
   1797 
   1798     $(this).append(
   1799       $('<div>').addClass('image')
   1800         .css('background-image', 'url(' + imgUrl + ')'),
   1801       $('<div>').addClass('info').append(
   1802         $('<h4>').addClass('title').html(resource.title_highlighted || resource.title),
   1803         $('<p>').addClass('summary').html(resource.summary),
   1804         $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More')
   1805       )
   1806     );
   1807 
   1808     return this;
   1809   };
   1810 })(jQuery);
   1811 
   1812 /*
   1813   Fullscreen Carousel
   1814 
   1815   The following allows for an area at the top of the page that takes over the
   1816   entire browser height except for its top offset and an optional bottom
   1817   padding specified as a data attribute.
   1818 
   1819   HTML:
   1820 
   1821   <div class="fullscreen-carousel">
   1822     <div class="fullscreen-carousel-content">
   1823       <!-- content here -->
   1824     </div>
   1825     <div class="fullscreen-carousel-content">
   1826       <!-- content here -->
   1827     </div>
   1828 
   1829     etc ...
   1830 
   1831   </div>
   1832 
   1833   Control over how the carousel takes over the screen can mostly be defined in
   1834   a css file. Setting min-height on the .fullscreen-carousel-content elements
   1835   will prevent them from shrinking to far vertically when the browser is very
   1836   short, and setting max-height on the .fullscreen-carousel itself will prevent
   1837   the area from becoming to long in the case that the browser is stretched very
   1838   tall.
   1839 
   1840   There is limited functionality for having multiple sections since that request
   1841   was removed, but it is possible to add .next-arrow and .prev-arrow elements to
   1842   scroll between multiple content areas.
   1843 */
   1844 
   1845 (function() {
   1846   $(document).ready(function() {
   1847     $('.fullscreen-carousel').each(function() {
   1848       initWidget(this);
   1849     });
   1850   });
   1851 
   1852   function initWidget(widget) {
   1853     var $widget = $(widget);
   1854 
   1855     var topOffset = $widget.offset().top;
   1856     var padBottom = parseInt($widget.data('paddingbottom')) || 0;
   1857     var maxHeight = 0;
   1858     var minHeight = 0;
   1859     var $content = $widget.find('.fullscreen-carousel-content');
   1860     var $nextArrow = $widget.find('.next-arrow');
   1861     var $prevArrow = $widget.find('.prev-arrow');
   1862     var $curSection = $($content[0]);
   1863 
   1864     if ($content.length <= 1) {
   1865       $nextArrow.hide();
   1866       $prevArrow.hide();
   1867     } else {
   1868       $nextArrow.click(function() {
   1869         var index = ($content.index($curSection) + 1);
   1870         $curSection.hide();
   1871         $curSection = $($content[index >= $content.length ? 0 : index]);
   1872         $curSection.show();
   1873       });
   1874 
   1875       $prevArrow.click(function() {
   1876         var index = ($content.index($curSection) - 1);
   1877         $curSection.hide();
   1878         $curSection = $($content[index < 0 ? $content.length - 1 : 0]);
   1879         $curSection.show();
   1880       });
   1881     }
   1882 
   1883     // Just hide all content sections except first.
   1884     $content.each(function(index) {
   1885       if ($(this).height() > minHeight) minHeight = $(this).height();
   1886       $(this).css({position: 'absolute',  display: index > 0 ? 'none' : ''});
   1887     });
   1888 
   1889     // Register for changes to window size, and trigger.
   1890     $(window).resize(resizeWidget);
   1891     resizeWidget();
   1892 
   1893     function resizeWidget() {
   1894       var height = $(window).height() - topOffset - padBottom;
   1895       $widget.width($(window).width());
   1896       $widget.height(height < minHeight ? minHeight :
   1897         (maxHeight && height > maxHeight ? maxHeight : height));
   1898     }
   1899   }
   1900 })();
   1901 
   1902 /*
   1903   Tab Carousel
   1904 
   1905   The following allows tab widgets to be installed via the html below. Each
   1906   tab content section should have a data-tab attribute matching one of the
   1907   nav items'. Also each tab content section should have a width matching the
   1908   tab carousel.
   1909 
   1910   HTML:
   1911 
   1912   <div class="tab-carousel">
   1913     <ul class="tab-nav">
   1914       <li><a href="#" data-tab="handsets">Handsets</a>
   1915       <li><a href="#" data-tab="wearable">Wearable</a>
   1916       <li><a href="#" data-tab="tv">TV</a>
   1917     </ul>
   1918 
   1919     <div class="tab-carousel-content">
   1920       <div data-tab="handsets">
   1921         <!--Full width content here-->
   1922       </div>
   1923 
   1924       <div data-tab="wearable">
   1925         <!--Full width content here-->
   1926       </div>
   1927 
   1928       <div data-tab="tv">
   1929         <!--Full width content here-->
   1930       </div>
   1931     </div>
   1932   </div>
   1933 
   1934 */
   1935 (function() {
   1936   $(document).ready(function() {
   1937     $('.tab-carousel').each(function() {
   1938       initWidget(this);
   1939     });
   1940   });
   1941 
   1942   function initWidget(widget) {
   1943     var $widget = $(widget);
   1944     var $nav = $widget.find('.tab-nav');
   1945     var $anchors = $nav.find('[data-tab]');
   1946     var $li = $nav.find('li');
   1947     var $contentContainer = $widget.find('.tab-carousel-content');
   1948     var $tabs = $contentContainer.find('[data-tab]');
   1949     var $curTab = $($tabs[0]); // Current tab is first tab.
   1950     var width = $widget.width();
   1951 
   1952     // Setup nav interactivity.
   1953     $anchors.click(function(evt) {
   1954       evt.preventDefault();
   1955       var query = '[data-tab=' + $(this).data('tab') + ']';
   1956       transitionWidget($tabs.filter(query));
   1957     });
   1958 
   1959     // Add highlight for navigation on first item.
   1960     var $highlight = $('<div>').addClass('highlight')
   1961       .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'})
   1962       .appendTo($nav);
   1963 
   1964     // Store height since we will change contents to absolute.
   1965     $contentContainer.height($contentContainer.height());
   1966 
   1967     // Absolutely position tabs so they're ready for transition.
   1968     $tabs.each(function(index) {
   1969       $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'});
   1970     });
   1971 
   1972     function transitionWidget($toTab) {
   1973       if (!$curTab.is($toTab)) {
   1974         var curIndex = $tabs.index($curTab[0]);
   1975         var toIndex = $tabs.index($toTab[0]);
   1976         var dir = toIndex > curIndex ? 1 : -1;
   1977 
   1978         // Animate content sections.
   1979         $toTab.css({left:(width * dir) + 'px'});
   1980         $curTab.animate({left:(width * -dir) + 'px'});
   1981         $toTab.animate({left:'0'});
   1982 
   1983         // Animate navigation highlight.
   1984         $highlight.animate({left:$($li[toIndex]).position().left + 'px',
   1985           width:$($li[toIndex]).outerWidth() + 'px'})
   1986 
   1987         // Store new current section.
   1988         $curTab = $toTab;
   1989       }
   1990     }
   1991   }
   1992 })();
   1993 
   1994 /**
   1995  * Auto TOC
   1996  *
   1997  * Upgrades h2s on the page to have a rule and be toggle-able on mobile.
   1998  */
   1999 (function($) {
   2000   var upgraded = false;
   2001   var h2Titles;
   2002 
   2003   function initWidget() {
   2004     // add HRs below all H2s (except for a few other h2 variants)
   2005     // Consider doing this with css instead.
   2006     h2Titles = $('h2').not('#qv h2, #tb h2, .sidebox h2, #devdoc-nav h2, h2.norule');
   2007     h2Titles.css({paddingBottom:0}).after('<hr/>');
   2008 
   2009     // Exit early if on older browser.
   2010     if (!window.matchMedia) {
   2011       return;
   2012     }
   2013 
   2014     // Only run logic in mobile layout.
   2015     var query = window.matchMedia('(max-width: 719px)');
   2016     if (query.matches) {
   2017       makeTogglable();
   2018     } else {
   2019       query.addListener(makeTogglable);
   2020     }
   2021   }
   2022 
   2023   function makeTogglable() {
   2024     // Only run this logic once.
   2025     if (upgraded) { return; }
   2026     upgraded = true;
   2027 
   2028     // Only make content h2s togglable.
   2029     var contentTitles = h2Titles.filter('#jd-content *');
   2030 
   2031     // If there are more than 1
   2032     if (contentTitles.size() < 2) {
   2033       return;
   2034     }
   2035 
   2036     contentTitles.each(function() {
   2037       // Find all the relevant nodes.
   2038       var $title = $(this);
   2039       var $hr = $title.next();
   2040       var $contents = allNextUntil($hr[0], 'h2, .next-docs');
   2041       var $section = $($title)
   2042         .add($hr)
   2043         .add($title.prev('a[name]'))
   2044         .add($contents);
   2045       var $anchor = $section.first().prev();
   2046       var anchorMethod = 'after';
   2047       if ($anchor.length === 0) {
   2048         $anchor = $title.parent();
   2049         anchorMethod = 'prepend';
   2050       }
   2051 
   2052       // Some h2s are in their own container making it pretty hard to find the end, so skip.
   2053       if ($contents.length === 0) {
   2054         return;
   2055       }
   2056 
   2057       // Remove from DOM before messing with it. DOM is slow!
   2058       $section.detach();
   2059 
   2060       // Add mobile-only expand arrows.
   2061       $title.prepend('<span class="dac-visible-mobile-inline-block">' +
   2062           '<i class="dac-toggle-expand dac-sprite dac-expand-more-black"></i>' +
   2063           '<i class="dac-toggle-collapse dac-sprite dac-expand-less-black"></i>' +
   2064           '</span>')
   2065         .attr('data-toggle', 'section');
   2066 
   2067       // Wrap in magic markup.
   2068       $section = $section.wrapAll('<div class="dac-toggle dac-mobile">').parent();
   2069 
   2070       // extra div used for max-height calculation.
   2071       $contents.wrapAll('<div class="dac-toggle-content dac-expand"><div>');
   2072 
   2073       // Pre-expand section if requested.
   2074       if ($title.hasClass('is-expanded')) {
   2075         $section.addClass('is-expanded');
   2076       }
   2077 
   2078       // Pre-expand section if targetted by hash.
   2079       if (location.hash && $section.find(location.hash).length) {
   2080         $section.addClass('is-expanded');
   2081       }
   2082 
   2083       // Add it back to the dom.
   2084       $anchor[anchorMethod].call($anchor, $section);
   2085     });
   2086   }
   2087 
   2088   // Similar to $.fn.nextUntil() except we need all nodes, jQuery skips text nodes.
   2089   function allNextUntil(elem, until) {
   2090     var matched = [];
   2091 
   2092     while ((elem = elem.nextSibling) && elem.nodeType !== 9) {
   2093       if (elem.nodeType === 1 && jQuery(elem).is(until)) {
   2094         break;
   2095       }
   2096       matched.push(elem);
   2097     }
   2098     return $(matched);
   2099   }
   2100 
   2101   $(function() {
   2102     initWidget();
   2103   });
   2104 })(jQuery);
   2105 
   2106 (function($, window) {
   2107   'use strict';
   2108 
   2109   // Blogger API info
   2110   var apiUrl = 'https://www.googleapis.com/blogger/v3';
   2111   var apiKey = 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8';
   2112 
   2113   // Blog IDs can be found in the markup of the blog posts
   2114   var blogs = {
   2115     'android-developers': {
   2116       id: '6755709643044947179',
   2117       title: 'Android Developers Blog'
   2118     }
   2119   };
   2120   var monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
   2121       'July', 'August', 'September', 'October', 'November', 'December'];
   2122 
   2123   var BlogReader = (function() {
   2124     var reader;
   2125 
   2126     function BlogReader() {
   2127       this.doneSetup = false;
   2128     }
   2129 
   2130     /**
   2131      * Initialize the blog reader and modal.
   2132      */
   2133     BlogReader.prototype.setup = function() {
   2134       $('#jd-content').append(
   2135           '<div id="blog-reader" data-modal="blog-reader" class="dac-modal dac-has-small-header">' +
   2136             '<div class="dac-modal-container">' +
   2137               '<div class="dac-modal-window">' +
   2138                 '<header class="dac-modal-header">' +
   2139                   '<div class="dac-modal-header-actions">' +
   2140                     '<a href="" class="dac-modal-header-open" target="_blank">' +
   2141                       '<i class="dac-sprite dac-open-in-new"></i>' +
   2142                     '</a>' +
   2143                     '<button class="dac-modal-header-close" data-modal-toggle>' +
   2144                     '</button>' +
   2145                   '</div>' +
   2146                   '<h2 class="norule dac-modal-header-title"></h2>' +
   2147                 '</header>' +
   2148                 '<div class="dac-modal-content dac-blog-reader">' +
   2149                   '<time class="dac-blog-reader-date" pubDate></time>' +
   2150                   '<h3 class="dac-blog-reader-title"></h3>' +
   2151                   '<div class="dac-blog-reader-text clearfix"></div>' +
   2152                 '</div>' +
   2153               '</div>' +
   2154             '</div>' +
   2155           '</div>');
   2156 
   2157       this.blogReader = $('#blog-reader').dacModal();
   2158 
   2159       this.doneSetup = true;
   2160     };
   2161 
   2162     BlogReader.prototype.openModal_ = function(blog, post) {
   2163       var published = new Date(post.published);
   2164       var formattedDate = monthNames[published.getMonth()] + ' ' + published.getDate() + ' ' + published.getFullYear();
   2165       this.blogReader.find('.dac-modal-header-open').attr('href', post.url);
   2166       this.blogReader.find('.dac-modal-header-title').text(blog.title);
   2167       this.blogReader.find('.dac-blog-reader-title').html(post.title);
   2168       this.blogReader.find('.dac-blog-reader-date').html(formattedDate);
   2169       this.blogReader.find('.dac-blog-reader-text').html(post.content);
   2170       this.blogReader.trigger('modal-open');
   2171     };
   2172 
   2173     /**
   2174      * Show a blog post in a modal
   2175      * @param  {string} blogName - The name of the Blogspot blog.
   2176      * @param  {string} postPath - The path to the blog post.
   2177      * @param  {bool} secondTry - Has it failed once?
   2178      */
   2179     BlogReader.prototype.showPost = function(blogName, postPath, secondTry) {
   2180       var blog = blogs[blogName];
   2181       var postUrl = 'https://' + blogName + '.blogspot.com' + postPath;
   2182 
   2183       var url = apiUrl + '/blogs/' + blog.id + '/posts/bypath?path=' + encodeURIComponent(postPath) + '&key=' + apiKey;
   2184       $.ajax(url, {timeout: 650}).done(this.openModal_.bind(this, blog)).fail(function(error) {
   2185         // Retry once if we get an error
   2186         if (error.status === 500 && !secondTry) {
   2187           this.showPost(blogName, postPath, true);
   2188         } else {
   2189           window.location.href = postUrl;
   2190         }
   2191       }.bind(this));
   2192     };
   2193 
   2194     return {
   2195       getReader: function() {
   2196         if (!reader) {
   2197           reader = new BlogReader();
   2198         }
   2199         return reader;
   2200       }
   2201     };
   2202   })();
   2203 
   2204   var blogReader = BlogReader.getReader();
   2205 
   2206   function wrapLinkWithReader(e) {
   2207     var el = $(e.currentTarget);
   2208     if (el.hasClass('dac-modal-header-open')) {
   2209       return;
   2210     }
   2211 
   2212     // Only catch links on blogspot.com
   2213     var matches = el.attr('href').match(/https?:\/\/([^\.]*).blogspot.com([^$]*)/);
   2214     if (matches && matches.length === 3) {
   2215       var blogName = matches[1];
   2216       var postPath = matches[2];
   2217 
   2218       // Check if we have information about the blog
   2219       if (!blogs[blogName]) {
   2220         return;
   2221       }
   2222 
   2223       // Setup the first time it's used
   2224       if (!blogReader.doneSetup) {
   2225         blogReader.setup();
   2226       }
   2227 
   2228       e.preventDefault();
   2229       blogReader.showPost(blogName, postPath);
   2230     }
   2231   }
   2232 
   2233   $(document).on('click.blog-reader', 'a.resource-card[href*="blogspot.com/"]',
   2234       wrapLinkWithReader);
   2235 })(jQuery, window);
   2236 
   2237 (function($) {
   2238   $.fn.debounce = function(func, wait, immediate) {
   2239     var timeout;
   2240 
   2241     return function() {
   2242       var context = this;
   2243       var args = arguments;
   2244 
   2245       var later = function() {
   2246         timeout = null;
   2247         if (!immediate) {
   2248           func.apply(context, args);
   2249         }
   2250       };
   2251 
   2252       var callNow = immediate && !timeout;
   2253       clearTimeout(timeout);
   2254       timeout = setTimeout(later, wait);
   2255 
   2256       if (callNow) {
   2257         func.apply(context, args);
   2258       }
   2259     };
   2260   };
   2261 })(jQuery);
   2262 
   2263 /* Calculate the vertical area remaining */
   2264 (function($) {
   2265   $.fn.ellipsisfade = function() {
   2266     // Only fetch line-height of first element to avoid recalculate style.
   2267     // Will be NaN if no elements match, which is ok.
   2268     var lineHeight = parseInt(this.css('line-height'), 10);
   2269 
   2270     this.each(function() {
   2271       // get element text
   2272       var $this = $(this);
   2273       var remainingHeight = $this.parent().parent().height();
   2274       $this.parent().siblings().each(function() {
   2275         var elHeight;
   2276         if ($(this).is(':visible')) {
   2277           elHeight = $(this).outerHeight(true);
   2278           remainingHeight = remainingHeight - elHeight;
   2279         }
   2280       });
   2281 
   2282       var adjustedRemainingHeight = ((remainingHeight) / lineHeight >> 0) * lineHeight;
   2283       $this.parent().css({height: adjustedRemainingHeight});
   2284       $this.css({height: 'auto'});
   2285     });
   2286 
   2287     return this;
   2288   };
   2289 
   2290   /* Pass the line height to ellipsisfade() to adjust the height of the
   2291    text container to show the max number of lines possible, without
   2292    showing lines that are cut off. This works with the css ellipsis
   2293    classes to fade last text line and apply an ellipsis char. */
   2294   function updateEllipsis(context) {
   2295     if (!(context instanceof jQuery)) {
   2296       context = $('html');
   2297     }
   2298 
   2299     context.find('.card-info .text').ellipsisfade();
   2300   }
   2301 
   2302   $(window).on('resize', $.fn.debounce(updateEllipsis, 500));
   2303   $(updateEllipsis);
   2304   $('html').on('dac:domchange', function(e) { updateEllipsis($(e.target)); });
   2305 })(jQuery);
   2306 
   2307 /* Filter */
   2308 (function($) {
   2309   'use strict';
   2310 
   2311   /**
   2312    * A single filter item content.
   2313    * @type {string} - Element template.
   2314    * @private
   2315    */
   2316   var ITEM_STR_ = '<input type="checkbox" value="{{value}}" class="dac-form-checkbox" id="{{id}}">' +
   2317       '<label for="{{id}}" class="dac-form-checkbox-button"></label>' +
   2318       '<label for="{{id}}" class="dac-form-label">{{name}}</label>';
   2319 
   2320   /**
   2321    * Template for a chip element.
   2322    * @type {*|HTMLElement}
   2323    * @private
   2324    */
   2325   var CHIP_BASE_ = $('<li class="dac-filter-chip">' +
   2326     '<button class="dac-filter-chip-close">' +
   2327       '<i class="dac-sprite dac-close-black dac-filter-chip-close-icon"></i>' +
   2328     '</button>' +
   2329   '</li>');
   2330 
   2331   /**
   2332    * Component to handle narrowing down resources.
   2333    * @param {HTMLElement} el - The DOM element.
   2334    * @param {Object} options
   2335    * @constructor
   2336    */
   2337   function Filter(el, options) {
   2338     this.el = $(el);
   2339     this.options = $.extend({}, Filter.DEFAULTS_, options);
   2340     this.init();
   2341   }
   2342 
   2343   Filter.DEFAULTS_ = {
   2344     activeClass: 'dac-active',
   2345     chipsDataAttr: 'filter-chips',
   2346     nameDataAttr: 'filter-name',
   2347     countDataAttr: 'filter-count',
   2348     tabViewDataAttr: 'tab-view',
   2349     valueDataAttr: 'filter-value'
   2350   };
   2351 
   2352   /**
   2353    * Draw resource cards.
   2354    * @param {Array} resources
   2355    * @private
   2356    */
   2357   Filter.prototype.draw_ = function(resources) {
   2358     var that = this;
   2359 
   2360     if (resources.length === 0) {
   2361       this.containerEl_.html('<p class="dac-filter-message">Nothing matches selected filters.</p>');
   2362       return;
   2363     }
   2364 
   2365     // Draw resources.
   2366     that.containerEl_.resourceWidget(resources, that.data_.options);
   2367   };
   2368 
   2369   /**
   2370    * Initialize a Filter component.
   2371    */
   2372   Filter.prototype.init = function() {
   2373     this.containerEl_ = $(this.options.filter);
   2374 
   2375     // Setup data settings
   2376     this.data_ = {};
   2377     this.data_.chips = {};
   2378     this.data_.options = this.containerEl_.widgetOptions();
   2379     this.data_.all = window.metadata.query(this.data_.options);
   2380 
   2381     // Initialize filter UI
   2382     this.initUi();
   2383   };
   2384 
   2385   /**
   2386    * Generate a chip for a given filter item.
   2387    * @param {Object} item - A single filter option (checkbox container).
   2388    * @returns {HTMLElement} A new Chip element.
   2389    */
   2390   Filter.prototype.chipForItem = function(item) {
   2391     var chip = CHIP_BASE_.clone();
   2392     chip.prepend(this.data_.chips[item.data('filter-value')]);
   2393     chip.data('item.dac-filter', item);
   2394     item.data('chip.dac-filter', chip);
   2395     this.addToItemValue(item, 1);
   2396     return chip[0];
   2397   };
   2398 
   2399   /**
   2400    * Update count of checked filter items.
   2401    * @param {Object} item - A single filter option (checkbox container).
   2402    * @param {Number} value - Either -1 or 1.
   2403    */
   2404   Filter.prototype.addToItemValue = function(item, value) {
   2405     var tab = item.parent().data(this.options.tabViewDataAttr);
   2406     var countEl = this.countEl_.filter('[data-' + this.options.countDataAttr + '="' + tab + '"]');
   2407     var count = value + parseInt(countEl.text(), 10);
   2408     countEl.text(count);
   2409     countEl.toggleClass('dac-disabled', count === 0);
   2410   };
   2411 
   2412   /**
   2413    * Set event listeners.
   2414    * @private
   2415    */
   2416   Filter.prototype.setEventListeners_ = function() {
   2417     this.chipsEl_.on('click.dac-filter', '.dac-filter-chip-close', this.closeChipHandler_.bind(this));
   2418     this.tabViewEl_.on('change.dac-filter', ':checkbox', this.toggleCheckboxHandler_.bind(this));
   2419   };
   2420 
   2421   /**
   2422    * Check filter items that are active by default.
   2423    */
   2424   Filter.prototype.activateInitialFilters_ = function() {
   2425     var id = (new Date()).getTime();
   2426     var initiallyCheckedValues = this.data_.options.query.replace(/,\s*/g, '+').split('+');
   2427     var chips = document.createDocumentFragment();
   2428     var that = this;
   2429 
   2430     this.items_.each(function(i) {
   2431       var item = $(this);
   2432       var opts = item.data();
   2433       that.data_.chips[opts.filterValue] = opts.filterName;
   2434 
   2435       var checkbox = $(ITEM_STR_.replace(/\{\{name\}\}/g, opts.filterName)
   2436         .replace(/\{\{value\}\}/g, opts.filterValue)
   2437         .replace(/\{\{id\}\}/g, 'filter-' + id + '-' + (i + 1)));
   2438 
   2439       if (initiallyCheckedValues.indexOf(opts.filterValue) > -1) {
   2440         checkbox[0].checked = true;
   2441         chips.appendChild(that.chipForItem(item));
   2442       }
   2443 
   2444       item.append(checkbox);
   2445     });
   2446 
   2447     this.chipsEl_.append(chips);
   2448   };
   2449 
   2450   /**
   2451    * Initialize the Filter view
   2452    */
   2453   Filter.prototype.initUi = function() {
   2454     // Cache DOM elements
   2455     this.chipsEl_ = this.el.find('[data-' + this.options.chipsDataAttr + ']');
   2456     this.countEl_ = this.el.find('[data-' + this.options.countDataAttr + ']');
   2457     this.tabViewEl_ = this.el.find('[data-' + this.options.tabViewDataAttr + ']');
   2458     this.items_ = this.el.find('[data-' + this.options.nameDataAttr + ']');
   2459 
   2460     // Setup UI
   2461     this.draw_(this.data_.all);
   2462     this.activateInitialFilters_();
   2463     this.setEventListeners_();
   2464   };
   2465 
   2466   /**
   2467    * @returns {[types|Array, tags|Array, category|Array]}
   2468    */
   2469   Filter.prototype.getActiveClauses = function() {
   2470     var tags = [];
   2471     var types = [];
   2472     var categories = [];
   2473 
   2474     this.items_.find(':checked').each(function(i, checkbox) {
   2475       // Currently, there is implicit business logic here that `tag` is AND'ed together
   2476       // while `type` is OR'ed. So , and + do the same thing here. It would be great to
   2477       // reuse the same query engine for filters, but it would need more powerful syntax.
   2478       // Probably parenthesis, to support "tag:dog + tag:cat + (type:video, type:blog)"
   2479       var expression = $(checkbox).val();
   2480       var regex = /(\w+):(\w+)/g;
   2481       var match;
   2482 
   2483       while (match = regex.exec(expression)) {
   2484         switch (match[1]) {
   2485           case 'category':
   2486             categories.push(match[2]);
   2487             break;
   2488           case 'tag':
   2489             tags.push(match[2]);
   2490             break;
   2491           case 'type':
   2492             types.push(match[2]);
   2493             break;
   2494         }
   2495       }
   2496     });
   2497 
   2498     return [types, tags, categories];
   2499   };
   2500 
   2501   /**
   2502    * Actual filtering logic.
   2503    * @returns {Array}
   2504    */
   2505   Filter.prototype.filteredResources = function() {
   2506     var data = this.getActiveClauses();
   2507     var types = data[0];
   2508     var tags = data[1];
   2509     var categories = data[2];
   2510     var resources = [];
   2511     var resource = {};
   2512     var tag = '';
   2513     var shouldAddResource = true;
   2514 
   2515     for (var resourceIndex = 0; resourceIndex < this.data_.all.length; resourceIndex++) {
   2516       resource = this.data_.all[resourceIndex];
   2517       shouldAddResource = types.indexOf(resource.type) > -1;
   2518 
   2519       if (categories && categories.length > 0) {
   2520         shouldAddResource = shouldAddResource && categories.indexOf(resource.category) > -1;
   2521       }
   2522 
   2523       for (var tagIndex = 0; shouldAddResource && tagIndex < tags.length; tagIndex++) {
   2524         tag = tags[tagIndex];
   2525         shouldAddResource = resource.tags.indexOf(tag) > -1;
   2526       }
   2527 
   2528       if (shouldAddResource) {
   2529         resources.push(resource);
   2530       }
   2531     }
   2532 
   2533     return resources;
   2534   };
   2535 
   2536   /**
   2537    * Close Chip Handler
   2538    * @param {Event} event - Click event
   2539    * @private
   2540    */
   2541   Filter.prototype.closeChipHandler_ = function(event) {
   2542     var chip = $(event.currentTarget).parent();
   2543     var checkbox = chip.data('item.dac-filter').find(':first-child')[0];
   2544     checkbox.checked = false;
   2545     this.changeStateForCheckbox(checkbox);
   2546   };
   2547 
   2548   /**
   2549    * Handle filter item state change.
   2550    * @param {Event} event - Change event
   2551    * @private
   2552    */
   2553   Filter.prototype.toggleCheckboxHandler_ = function(event) {
   2554     this.changeStateForCheckbox(event.currentTarget);
   2555   };
   2556 
   2557   /**
   2558    * Redraw resource view based on new state.
   2559    * @param checkbox
   2560    */
   2561   Filter.prototype.changeStateForCheckbox = function(checkbox) {
   2562     var item = $(checkbox).parent();
   2563 
   2564     if (checkbox.checked) {
   2565       this.chipsEl_.append(this.chipForItem(item));
   2566       devsite.analytics.trackAnalyticsEvent('event',
   2567           'Filters', 'Check', $(checkbox).val());
   2568     } else {
   2569       item.data('chip.dac-filter').remove();
   2570       this.addToItemValue(item, -1);
   2571       devsite.analytics.trackAnalyticsEvent('event',
   2572           'Filters', 'Uncheck', $(checkbox).val());
   2573     }
   2574 
   2575     this.draw_(this.filteredResources());
   2576   };
   2577 
   2578   /**
   2579    * jQuery plugin
   2580    */
   2581   $.fn.dacFilter = function() {
   2582     return this.each(function() {
   2583       var el = $(this);
   2584       new Filter(el, el.data());
   2585     });
   2586   };
   2587 
   2588   /**
   2589    * Data Attribute API
   2590    */
   2591   $(function() {
   2592     $('[data-filter]').dacFilter();
   2593   });
   2594 })(jQuery);
   2595 
   2596 (function($) {
   2597   'use strict';
   2598 
   2599   /**
   2600    * Toggle Floating Label state.
   2601    * @param {HTMLElement} el - The DOM element.
   2602    * @param options
   2603    * @constructor
   2604    */
   2605   function FloatingLabel(el, options) {
   2606     this.el = $(el);
   2607     this.options = $.extend({}, FloatingLabel.DEFAULTS_, options);
   2608     this.group = this.el.closest('.dac-form-input-group');
   2609     this.input = this.group.find('.dac-form-input');
   2610 
   2611     this.checkValue_ = this.checkValue_.bind(this);
   2612     this.checkValue_();
   2613 
   2614     this.input.on('focus', function() {
   2615       this.group.addClass('dac-focused');
   2616     }.bind(this));
   2617     this.input.on('blur', function() {
   2618       this.group.removeClass('dac-focused');
   2619       this.checkValue_();
   2620     }.bind(this));
   2621     this.input.on('keyup', this.checkValue_);
   2622   }
   2623 
   2624   /**
   2625    * The label is moved out of the textbox when it has a value.
   2626    */
   2627   FloatingLabel.prototype.checkValue_ = function() {
   2628     if (this.input.val().length) {
   2629       this.group.addClass('dac-has-value');
   2630     } else {
   2631       this.group.removeClass('dac-has-value');
   2632     }
   2633   };
   2634 
   2635   /**
   2636    * jQuery plugin
   2637    * @param  {object} options - Override default options.
   2638    */
   2639   $.fn.dacFloatingLabel = function(options) {
   2640     return this.each(function() {
   2641       new FloatingLabel(this, options);
   2642     });
   2643   };
   2644 
   2645   $(document).on('ready.aranja', function() {
   2646     $('.dac-form-floatlabel').each(function() {
   2647       $(this).dacFloatingLabel($(this).data());
   2648     });
   2649   });
   2650 })(jQuery);
   2651 
   2652 (function($) {
   2653   'use strict';
   2654 
   2655   /**
   2656    * @param {HTMLElement} el - The DOM element.
   2657    * @param {Object} options
   2658    * @constructor
   2659    */
   2660   function Crumbs(selected, options) {
   2661     this.options = $.extend({}, Crumbs.DEFAULTS_, options);
   2662     this.el = $(this.options.container);
   2663 
   2664     // Do not build breadcrumbs for landing site.
   2665     if (!selected || location.pathname === '/index.html' || location.pathname === '/') {
   2666       return;
   2667     }
   2668 
   2669     // Cache navigation resources
   2670     this.selected = $(selected);
   2671     this.selectedParent = this.selected.closest('.dac-nav-secondary').siblings('a');
   2672 
   2673     // Build the breadcrumb list.
   2674     this.init();
   2675   }
   2676 
   2677   Crumbs.DEFAULTS_ = {
   2678     container: '.dac-header-crumbs',
   2679     crumbItem: $('<li class="dac-header-crumbs-item">'),
   2680     linkClass: 'dac-header-crumbs-link'
   2681   };
   2682 
   2683   Crumbs.prototype.init = function() {
   2684     Crumbs.buildCrumbForLink(this.selected.clone()).appendTo(this.el);
   2685 
   2686     if (this.selectedParent.length) {
   2687       Crumbs.buildCrumbForLink(this.selectedParent.clone()).prependTo(this.el);
   2688     }
   2689 
   2690     // Reveal the breadcrumbs
   2691     this.el.addClass('dac-has-content');
   2692   };
   2693 
   2694   /**
   2695    * Build a HTML structure for a breadcrumb.
   2696    * @param {string} link
   2697    * @return {jQuery}
   2698    */
   2699   Crumbs.buildCrumbForLink = function(link) {
   2700     link.find('br').replaceWith(' ');
   2701 
   2702     var crumbLink = $('<a>')
   2703       .attr('class', Crumbs.DEFAULTS_.linkClass)
   2704       .attr('href', link.attr('href'))
   2705       .text(link.text());
   2706 
   2707     return Crumbs.DEFAULTS_.crumbItem.clone().append(crumbLink);
   2708   };
   2709 
   2710   /**
   2711    * jQuery plugin
   2712    */
   2713   $.fn.dacCrumbs = function(options) {
   2714     return this.each(function() {
   2715       new Crumbs(this, options);
   2716     });
   2717   };
   2718 })(jQuery);
   2719 
   2720 (function($) {
   2721   'use strict';
   2722 
   2723   /**
   2724    * @param {HTMLElement} el - The DOM element.
   2725    * @param {Object} options
   2726    * @constructor
   2727    */
   2728   function SearchInput(el, options) {
   2729     this.el = $(el);
   2730     this.options = $.extend({}, SearchInput.DEFAULTS_, options);
   2731     this.body = $('body');
   2732     this.input = this.el.find('input');
   2733     this.close = this.el.find(this.options.closeButton);
   2734     this.clear = this.el.find(this.options.clearButton);
   2735     this.icon = this.el.find('.' + this.options.iconClass);
   2736     this.init();
   2737   }
   2738 
   2739   SearchInput.DEFAULTS_ = {
   2740     activeClass: 'dac-active',
   2741     activeIconClass: 'dac-search',
   2742     closeButton: '[data-search-close]',
   2743     clearButton: '[data-search-clear]',
   2744     hiddenClass: 'dac-hidden',
   2745     iconClass: 'dac-header-search-icon',
   2746     searchModeClass: 'dac-search-mode',
   2747     transitionDuration: 250
   2748   };
   2749 
   2750   SearchInput.prototype.init = function() {
   2751     this.input.on('focus.dac-search', this.setActiveState.bind(this))
   2752               .on('input.dac-search', this.checkInputValue.bind(this));
   2753     this.close.on('click.dac-search', this.unsetActiveStateHandler_.bind(this));
   2754     this.clear.on('click.dac-search', this.clearInput.bind(this));
   2755   };
   2756 
   2757   SearchInput.prototype.setActiveState = function() {
   2758     var that = this;
   2759 
   2760     this.clear.addClass(this.options.hiddenClass);
   2761     this.body.addClass(this.options.searchModeClass);
   2762     this.checkInputValue();
   2763 
   2764     // Set icon to black after background has faded to white.
   2765     setTimeout(function() {
   2766       that.icon.addClass(that.options.activeIconClass);
   2767     }, this.options.transitionDuration);
   2768   };
   2769 
   2770   SearchInput.prototype.unsetActiveStateHandler_ = function(event) {
   2771     event.preventDefault();
   2772     this.unsetActiveState();
   2773   };
   2774 
   2775   SearchInput.prototype.unsetActiveState = function() {
   2776     this.icon.removeClass(this.options.activeIconClass);
   2777     this.clear.addClass(this.options.hiddenClass);
   2778     this.body.removeClass(this.options.searchModeClass);
   2779   };
   2780 
   2781   SearchInput.prototype.clearInput = function(event) {
   2782     event.preventDefault();
   2783     this.input.val('');
   2784     this.clear.addClass(this.options.hiddenClass);
   2785   };
   2786 
   2787   SearchInput.prototype.checkInputValue = function() {
   2788     if (this.input.val().length) {
   2789       this.clear.removeClass(this.options.hiddenClass);
   2790     } else {
   2791       this.clear.addClass(this.options.hiddenClass);
   2792     }
   2793   };
   2794 
   2795   /**
   2796    * jQuery plugin
   2797    * @param {object} options - Override default options.
   2798    */
   2799   $.fn.dacSearchInput = function() {
   2800     return this.each(function() {
   2801       var el = $(this);
   2802       el.data('search-input.dac', new SearchInput(el, el.data()));
   2803     });
   2804   };
   2805 
   2806   /**
   2807    * Data Attribute API
   2808    */
   2809   $(function() {
   2810     $('[data-search]').dacSearchInput();
   2811   });
   2812 })(jQuery);
   2813 
   2814 /* global METADATA */
   2815 (function($) {
   2816   function DacCarouselQuery(el) {
   2817     el = $(el);
   2818 
   2819     var opts = el.data();
   2820     opts.maxResults = parseInt(opts.maxResults || '100', 10);
   2821     opts.query = opts.carouselQuery;
   2822     var resources = window.metadata.query(opts);
   2823 
   2824     el.empty();
   2825     $(resources).each(function() {
   2826       var resource = $.extend({}, this, METADATA.carousel[this.url]);
   2827       el.dacHero(resource);
   2828     });
   2829 
   2830     // Pagination element.
   2831     el.append('<div class="dac-hero-carousel-pagination"><div class="wrap" data-carousel-pagination>');
   2832 
   2833     el.dacCarousel();
   2834   }
   2835 
   2836   // jQuery plugin
   2837   $.fn.dacCarouselQuery = function() {
   2838     return this.each(function() {
   2839       var el = $(this);
   2840       var data = el.data('dac.carouselQuery');
   2841 
   2842       if (!data) { el.data('dac.carouselQuery', (data = new DacCarouselQuery(el))); }
   2843     });
   2844   };
   2845 
   2846   // Data API
   2847   $(function() {
   2848     $('[data-carousel-query]').dacCarouselQuery();
   2849   });
   2850 })(jQuery);
   2851 
   2852 (function($) {
   2853   /**
   2854    * A CSS based carousel, inspired by SequenceJS.
   2855    * @param {jQuery} el
   2856    * @param {object} options
   2857    * @constructor
   2858    */
   2859   function DacCarousel(el, options) {
   2860     this.el = $(el);
   2861     this.options = options = $.extend({}, DacCarousel.OPTIONS, this.el.data(), options || {});
   2862     this.frames = this.el.find(options.frameSelector);
   2863     this.count = this.frames.size();
   2864     this.current = options.start;
   2865 
   2866     this.initPagination();
   2867     this.initEvents();
   2868     this.initFrame();
   2869   }
   2870 
   2871   DacCarousel.OPTIONS = {
   2872     auto:      true,
   2873     autoTime:  10000,
   2874     autoMinTime: 5000,
   2875     btnPrev:   '[data-carousel-prev]',
   2876     btnNext:   '[data-carousel-next]',
   2877     frameSelector: 'article',
   2878     loop:      true,
   2879     start:     0,
   2880     swipeThreshold: 160,
   2881     pagination: '[data-carousel-pagination]'
   2882   };
   2883 
   2884   DacCarousel.prototype.initPagination = function() {
   2885     this.pagination = $([]);
   2886     if (!this.options.pagination) { return; }
   2887 
   2888     var pagination = $('<ul class="dac-pagination">');
   2889     var parent = this.el;
   2890     if (typeof this.options.pagination === 'string') { parent = this.el.find(this.options.pagination); }
   2891 
   2892     if (this.count > 1) {
   2893       for (var i = 0; i < this.count; i++) {
   2894         var li = $('<li class="dac-pagination-item">').text(i);
   2895         if (i === this.options.start) { li.addClass('active'); }
   2896         li.click(this.go.bind(this, i));
   2897 
   2898         pagination.append(li);
   2899       }
   2900       this.pagination = pagination.children();
   2901       parent.append(pagination);
   2902     }
   2903   };
   2904 
   2905   DacCarousel.prototype.initEvents = function() {
   2906     var that = this;
   2907 
   2908     this.touch = {
   2909       start: {x: 0, y: 0},
   2910       end:   {x: 0, y: 0}
   2911     };
   2912 
   2913     this.el.on('touchstart', this.touchstart_.bind(this));
   2914     this.el.on('touchend', this.touchend_.bind(this));
   2915     this.el.on('touchmove', this.touchmove_.bind(this));
   2916 
   2917     this.el.hover(function() {
   2918       that.pauseRotateTimer();
   2919     }, function() {
   2920       that.startRotateTimer();
   2921     });
   2922 
   2923     $(this.options.btnPrev).click(function(e) {
   2924       e.preventDefault();
   2925       that.prev();
   2926     });
   2927 
   2928     $(this.options.btnNext).click(function(e) {
   2929       e.preventDefault();
   2930       that.next();
   2931     });
   2932   };
   2933 
   2934   DacCarousel.prototype.touchstart_ = function(event) {
   2935     var t = event.originalEvent.touches[0];
   2936     this.touch.start = {x: t.screenX, y: t.screenY};
   2937   };
   2938 
   2939   DacCarousel.prototype.touchend_ = function() {
   2940     var deltaX = this.touch.end.x - this.touch.start.x;
   2941     var deltaY = Math.abs(this.touch.end.y - this.touch.start.y);
   2942     var shouldSwipe = (deltaY < Math.abs(deltaX)) && (Math.abs(deltaX) >= this.options.swipeThreshold);
   2943 
   2944     if (shouldSwipe) {
   2945       if (deltaX > 0) {
   2946         this.prev();
   2947       } else {
   2948         this.next();
   2949       }
   2950     }
   2951   };
   2952 
   2953   DacCarousel.prototype.touchmove_ = function(event) {
   2954     var t = event.originalEvent.touches[0];
   2955     this.touch.end = {x: t.screenX, y: t.screenY};
   2956   };
   2957 
   2958   DacCarousel.prototype.initFrame = function() {
   2959     this.frames.removeClass('active').eq(this.options.start).addClass('active');
   2960   };
   2961 
   2962   DacCarousel.prototype.startRotateTimer = function() {
   2963     if (!this.options.auto || this.rotateTimer) { return; }
   2964     this.rotateTimer = setTimeout(this.next.bind(this), this.options.autoTime);
   2965   };
   2966 
   2967   DacCarousel.prototype.pauseRotateTimer = function() {
   2968     clearTimeout(this.rotateTimer);
   2969     this.rotateTimer = null;
   2970   };
   2971 
   2972   DacCarousel.prototype.prev = function() {
   2973     this.go(this.current - 1);
   2974   };
   2975 
   2976   DacCarousel.prototype.next = function() {
   2977     this.go(this.current + 1);
   2978   };
   2979 
   2980   DacCarousel.prototype.go = function(next) {
   2981     // Figure out what the next slide is.
   2982     while (this.count > 0 && next >= this.count) { next -= this.count; }
   2983     while (next < 0) { next += this.count; }
   2984 
   2985     // Cancel if we're already on that slide.
   2986     if (next === this.current) { return; }
   2987 
   2988     // Prepare next slide.
   2989     this.frames.eq(next).removeClass('out');
   2990 
   2991     // Recalculate styles before starting slide transition.
   2992     this.el.resolveStyles();
   2993     // Update pagination
   2994     this.pagination.removeClass('active').eq(next).addClass('active');
   2995 
   2996     // Transition out current frame
   2997     this.frames.eq(this.current).toggleClass('active out');
   2998 
   2999     // Transition in a new frame
   3000     this.frames.eq(next).toggleClass('active');
   3001 
   3002     this.current = next;
   3003   };
   3004 
   3005   // Helper which resolves new styles for an element, so it can start transitioning
   3006   // from the new values.
   3007   $.fn.resolveStyles = function() {
   3008     /*jshint expr:true*/
   3009     this[0] && this[0].offsetTop;
   3010     return this;
   3011   };
   3012 
   3013   // jQuery plugin
   3014   $.fn.dacCarousel = function() {
   3015     this.each(function() {
   3016       var $el = $(this);
   3017       $el.data('dac-carousel', new DacCarousel(this));
   3018     });
   3019     return this;
   3020   };
   3021 
   3022   // Data API
   3023   $(function() {
   3024     $('[data-carousel]').dacCarousel();
   3025   });
   3026 })(jQuery);
   3027 
   3028 /* global toRoot */
   3029 
   3030 (function($) {
   3031   // Ordering matters
   3032   var TAG_MAP = [
   3033     {from: 'developerstory', to: 'Android Developer Story'},
   3034     {from: 'googleplay', to: 'Google Play'}
   3035   ];
   3036 
   3037   function DacHero(el, resource, isSearch) {
   3038     var slide = $('<article>');
   3039     slide.addClass(isSearch ? 'dac-search-hero' : 'dac-expand dac-hero');
   3040     var image = cleanUrl(resource.heroImage || resource.image);
   3041     var fullBleed = image && !resource.heroColor;
   3042 
   3043     if (!isSearch) {
   3044       // Configure background
   3045       slide.css({
   3046         backgroundImage: fullBleed ? 'url(' + image + ')' : '',
   3047         backgroundColor: resource.heroColor || ''
   3048       });
   3049 
   3050       // Should copy be inverted
   3051       slide.toggleClass('dac-invert', resource.heroInvert || fullBleed);
   3052       slide.toggleClass('dac-darken', fullBleed);
   3053 
   3054       // Should be clickable
   3055       slide.append($('<a class="dac-hero-carousel-action">').attr('href', cleanUrl(resource.url)));
   3056     }
   3057 
   3058     var cols = $('<div class="cols dac-hero-content">');
   3059 
   3060     // inline image column
   3061     var rightCol = $('<div class="col-1of2 col-push-1of2 dac-hero-figure">')
   3062       .appendTo(cols);
   3063 
   3064     if ((!fullBleed || isSearch) && image) {
   3065       rightCol.append($('<img>').attr('src', image));
   3066     }
   3067 
   3068     // info column
   3069     $('<div class="col-1of2 col-pull-1of2">')
   3070       .append($('<div class="dac-hero-tag">').text(formatTag(resource)))
   3071       .append($('<h1 class="dac-hero-title">').text(formatTitle(resource)))
   3072       .append($('<p class="dac-hero-description">').text(resource.summary))
   3073       .append($('<a class="dac-hero-cta">')
   3074         .text(formatCTA(resource))
   3075         .attr('href', cleanUrl(resource.url))
   3076         .prepend($('<span class="dac-sprite dac-auto-chevron">'))
   3077       )
   3078       .appendTo(cols);
   3079 
   3080     slide.append(cols.wrap('<div class="wrap">').parent());
   3081     el.append(slide);
   3082   }
   3083 
   3084   function cleanUrl(url) {
   3085     if (url && url.indexOf('//') === -1) {
   3086       url = toRoot + url;
   3087     }
   3088     return url;
   3089   }
   3090 
   3091   function formatTag(resource) {
   3092     // Hmm, need a better more scalable solution for this.
   3093     for (var i = 0, mapping; mapping = TAG_MAP[i]; i++) {
   3094       if (resource.tags.indexOf(mapping.from) > -1) {
   3095         return mapping.to;
   3096       }
   3097     }
   3098     return resource.type;
   3099   }
   3100 
   3101   function formatTitle(resource) {
   3102     return resource.title.replace(/android developer story: /i, '');
   3103   }
   3104 
   3105   function formatCTA(resource) {
   3106     return resource.type === 'youtube' ? 'Watch the video' : 'Learn more';
   3107   }
   3108 
   3109   // jQuery plugin
   3110   $.fn.dacHero = function(resource, isSearch) {
   3111     return this.each(function() {
   3112       var el = $(this);
   3113       return new DacHero(el, resource, isSearch);
   3114     });
   3115   };
   3116 })(jQuery);
   3117 
   3118 (function($) {
   3119   'use strict';
   3120 
   3121   function highlightString(label, query) {
   3122     query = query || '';
   3123     //query = query.replace('<wbr>', '').replace('.', '\\.');
   3124     var queryRE = new RegExp('(' + query + ')', 'ig');
   3125     return label.replace(queryRE, '<em>$1</em>');
   3126   }
   3127 
   3128   $.fn.highlightMatches = function(query) {
   3129     return this.each(function() {
   3130       var el = $(this);
   3131       var label = el.html();
   3132       var highlighted = highlightString(label, query);
   3133       el.html(highlighted);
   3134       el.addClass('highlighted');
   3135     });
   3136   };
   3137 })(jQuery);
   3138 
   3139 /**
   3140  * History tracking.
   3141  * Track visited urls in localStorage.
   3142  */
   3143 (function($) {
   3144   var PAGES_TO_STORE_ = 100;
   3145   var MIN_NUMBER_OF_PAGES_TO_DISPLAY_ = 6;
   3146   var CONTAINER_SELECTOR_ = '.dac-search-results-history-wrap';
   3147 
   3148   /**
   3149    * Generate resource cards for visited pages.
   3150    * @param {HTMLElement} el
   3151    * @constructor
   3152    */
   3153   function HistoryQuery(el) {
   3154     this.el = $(el);
   3155 
   3156     // Only show history component if enough pages have been visited.
   3157     if (getVisitedPages().length < MIN_NUMBER_OF_PAGES_TO_DISPLAY_) {
   3158       this.el.closest(CONTAINER_SELECTOR_).addClass('dac-hidden');
   3159       return;
   3160     }
   3161 
   3162     // Rename query
   3163     this.el.data('query', this.el.data('history-query'));
   3164 
   3165     // jQuery method to populate cards.
   3166     this.el.resourceWidget();
   3167   }
   3168 
   3169   /**
   3170    * Fetch from localStorage an array of visted pages
   3171    * @returns {Array}
   3172    */
   3173   function getVisitedPages() {
   3174     var visited = localStorage.getItem('visited-pages');
   3175     return visited ? JSON.parse(visited) : [];
   3176   }
   3177 
   3178   /**
   3179    * Return a page corresponding to cuurent pathname. If none exists, create one.
   3180    * @param {Array} pages
   3181    * @param {String} path
   3182    * @returns {Object} Page
   3183    */
   3184   function getPageForPath(pages, path) {
   3185     var page;
   3186 
   3187     // Backwards lookup for current page, last pages most likely to be visited again.
   3188     for (var i = pages.length - 1; i >= 0; i--) {
   3189       if (pages[i].path === path) {
   3190         page = pages[i];
   3191 
   3192         // Remove page object from pages list to ensure correct ordering.
   3193         pages.splice(i, 1);
   3194 
   3195         return page;
   3196       }
   3197     }
   3198 
   3199     // If storage limit is exceeded, remove last visited path.
   3200     if (pages.length >= PAGES_TO_STORE_) {
   3201       pages.shift();
   3202     }
   3203 
   3204     return {path: path};
   3205   }
   3206 
   3207   /**
   3208    * Add current page to back of visited array, increase hit count by 1.
   3209    */
   3210   function addCurrectPage() {
   3211     var path = location.pathname;
   3212 
   3213     // Do not track frontpage visits.
   3214     if (path === '/' || path === '/index.html') {return;}
   3215 
   3216     var pages = getVisitedPages();
   3217     var page = getPageForPath(pages, path);
   3218 
   3219     // New page visits have no hit count.
   3220     page.hit = ~~page.hit + 1;
   3221 
   3222     // Most recently visted pages are located at the end of the visited array.
   3223     pages.push(page);
   3224 
   3225     localStorage.setItem('visited-pages', JSON.stringify(pages));
   3226   }
   3227 
   3228   /**
   3229    * Hit count compare function.
   3230    * @param {Object} a - page
   3231    * @param {Object} b - page
   3232    * @returns {number}
   3233    */
   3234   function byHit(a, b) {
   3235     if (a.hit > b.hit) {
   3236       return -1;
   3237     } else if (a.hit < b.hit) {
   3238       return 1;
   3239     }
   3240 
   3241     return 0;
   3242   }
   3243 
   3244   /**
   3245    * Return a list of visited urls in a given order.
   3246    * @param {String} order - (recent|most-visited)
   3247    * @returns {Array}
   3248    */
   3249   $.dacGetVisitedUrls = function(order) {
   3250     var pages = getVisitedPages();
   3251 
   3252     if (order === 'recent') {
   3253       pages.reverse();
   3254     } else {
   3255       pages.sort(byHit);
   3256     }
   3257 
   3258     return pages.map(function(page) {
   3259       return page.path.replace(/^\//, '');
   3260     });
   3261   };
   3262 
   3263   // jQuery plugin
   3264   $.fn.dacHistoryQuery = function() {
   3265     return this.each(function() {
   3266       var el = $(this);
   3267       var data = el.data('dac.recentlyVisited');
   3268 
   3269       if (!data) {
   3270         el.data('dac.recentlyVisited', (data = new HistoryQuery(el)));
   3271       }
   3272     });
   3273   };
   3274 
   3275   $(function() {
   3276     $('[data-history-query]').dacHistoryQuery();
   3277     // Do not block page rendering.
   3278     setTimeout(addCurrectPage, 0);
   3279   });
   3280 })(jQuery);
   3281 
   3282 /* ############################################ */
   3283 /* ##########     LOCALIZATION     ############ */
   3284 /* ############################################ */
   3285 /**
   3286  * Global helpers.
   3287  */
   3288 function getBaseUri(uri) {
   3289   var intlUrl = (uri.substring(0, 6) === '/intl/');
   3290   if (intlUrl) {
   3291     var base = uri.substring(uri.indexOf('intl/') + 5, uri.length);
   3292     base = base.substring(base.indexOf('/') + 1, base.length);
   3293     return '/' + base;
   3294   } else {
   3295     return uri;
   3296   }
   3297 }
   3298 
   3299 function changeLangPref(targetLang, submit) {
   3300   window.writeCookie('pref_lang', targetLang, null);
   3301   $('#language').find('option[value="' + targetLang + '"]').attr('selected', true);
   3302   if (submit) {
   3303     $('#setlang').submit();
   3304   }
   3305 }
   3306 // Redundant usage to appease jshint.
   3307 window.changeLangPref = changeLangPref;
   3308 
   3309 (function() {
   3310   /**
   3311    * Whitelisted locales. Should match choices in language dropdown. Repeated here
   3312    * as a lot of i18n logic happens before page load and dropdown is ready.
   3313    */
   3314   var LANGUAGES = [
   3315     'en',
   3316     'es',
   3317     'id',
   3318     'ja',
   3319     'ko',
   3320     'pt-br',
   3321     'ru',
   3322     'vi',
   3323     'zh-cn',
   3324     'zh-tw'
   3325   ];
   3326 
   3327   /**
   3328    * Master list of translated strings for template files.
   3329    */
   3330   var PHRASES = {
   3331     'newsletter': {
   3332       'title': 'Get the latest Android developer news and tips that will help you find success on Google Play.',
   3333       'requiredHint': '* Required Fields',
   3334       'name': 'Full name',
   3335       'email': 'Email address',
   3336       'company': 'Company / developer name',
   3337       'appUrl': 'One of your Play Store app URLs',
   3338       'business': {
   3339         'label': 'Which best describes your business:',
   3340         'apps': 'Apps',
   3341         'games': 'Games',
   3342         'both': 'Apps & Games'
   3343       },
   3344       'confirmMailingList': 'Add me to the mailing list for the monthly newsletter and occasional emails about ' +
   3345                             'development and Google Play opportunities.',
   3346       'privacyPolicy': 'I acknowledge that the information provided in this form will be subject to Google\'s ' +
   3347                        '<a href="https://www.google.com/policies/privacy/" target="_blank">privacy policy</a>.',
   3348       'languageVal': 'English',
   3349       'successTitle': 'Hooray!',
   3350       'successDetails': 'You have successfully signed up for the latest Android developer news and tips.',
   3351       'languageValTarget': {
   3352         'en': 'English',
   3353         'ar': 'Arabic ()',
   3354         'id': 'Indonesian (Bahasa)',
   3355         'fr': 'French (franais)',
   3356         'de': 'German (Deutsch)',
   3357         'ja': 'Japanese ()',
   3358         'ko': 'Korean ()',
   3359         'ru': 'Russian ()',
   3360         'es': 'Spanish (espaol)',
   3361         'th': 'Thai ()',
   3362         'tr': 'Turkish (Trke)',
   3363         'vi': 'Vietnamese (ting Vit)',
   3364         'pt-br': 'Brazilian Portuguese (Portugus Brasileiro)',
   3365         'zh-cn': 'Simplified Chinese ()',
   3366         'zh-tw': 'Traditional Chinese ()',
   3367       },
   3368       'resetLangTitle': "Browse this site in %{targetLang}?",
   3369       'resetLangTextIntro': 'You requested a page in %{targetLang}, but your language preference for this site is %{lang}.',
   3370       'resetLangTextCta': 'Would you like to change your language preference and browse this site in %{targetLang}? ' +
   3371                           'If you want to change your language preference later, use the language menu at the bottom of each page.',
   3372       'resetLangButtonYes': 'Change Language',
   3373       'resetLangButtonNo': 'Not Now'
   3374     }
   3375   };
   3376 
   3377   /**
   3378    * Current locale.
   3379    */
   3380   var locale = (function() {
   3381     var lang = window.readCookie('pref_lang');
   3382     if (lang === 0 || LANGUAGES.indexOf(lang) === -1) {
   3383       lang = 'en';
   3384     }
   3385     return lang;
   3386   })();
   3387   var localeTarget = (function() {
   3388     var lang = getQueryVariable('hl');
   3389     if (lang === false || LANGUAGES.indexOf(lang) === -1) {
   3390       lang = locale;
   3391     }
   3392     return lang;
   3393   })();
   3394 
   3395   /**
   3396    * Global function shims for backwards compatibility
   3397    */
   3398   window.changeNavLang = function() {
   3399     // Already done.
   3400   };
   3401 
   3402   window.loadLangPref = function() {
   3403     // Languages pref already loaded.
   3404   };
   3405 
   3406   window.getLangPref = function() {
   3407     return locale;
   3408   };
   3409 
   3410   window.getLangTarget = function() {
   3411     return localeTarget;
   3412   };
   3413 
   3414   // Expose polyglot instance for advanced localization.
   3415   var polyglot = window.polyglot = new window.Polyglot({
   3416     locale: locale,
   3417     phrases: PHRASES
   3418   });
   3419 
   3420   // When DOM is ready.
   3421   $(function() {
   3422     // Mark current locale in language picker.
   3423     $('#language').find('option[value="' + locale + '"]').attr('selected', true);
   3424 
   3425     $('html').dacTranslate().on('dac:domchange', function(e) {
   3426       $(e.target).dacTranslate();
   3427     });
   3428   });
   3429 
   3430   $.fn.dacTranslate = function() {
   3431     // Translate strings in template markup:
   3432 
   3433     // OLD
   3434     // Having all translations in HTML does not scale well and bloats every page.
   3435     // Need to migrate this to data-l JS translations below.
   3436     if (locale !== 'en') {
   3437       var $links = this.find('a[' + locale + '-lang]');
   3438       $links.each(function() { // for each link with a translation
   3439         var $link = $(this);
   3440         // put the desired language from the attribute as the text
   3441         $link.text($link.attr(locale + '-lang'));
   3442       });
   3443     }
   3444 
   3445     // NEW
   3446     // A simple declarative api for JS translations. Feel free to extend as appropriate.
   3447 
   3448     // Miscellaneous string compilations
   3449     // Build full strings from localized substrings:
   3450     var myLocaleTarget = window.getLangTarget();
   3451     var myTargetLang = window.polyglot.t("newsletter.languageValTarget." + myLocaleTarget);
   3452     var myLang = window.polyglot.t("newsletter.languageVal");
   3453     var myTargetLangTitleString = window.polyglot.t("newsletter.resetLangTitle", {targetLang: myTargetLang});
   3454     var myResetLangTextIntro = window.polyglot.t("newsletter.resetLangTextIntro", {targetLang: myTargetLang, lang: myLang});
   3455     var myResetLangTextCta = window.polyglot.t("newsletter.resetLangTextCta", {targetLang: myTargetLang});
   3456     //var myResetLangButtonYes = window.polyglot.t("newsletter.resetLangButtonYes", {targetLang: myTargetLang});
   3457 
   3458     // Inject strings as text values in dialog components:
   3459     $("#langform .dac-modal-header-title").text(myTargetLangTitleString);
   3460     $("#langform #resetLangText").text(myResetLangTextIntro);
   3461     $("#langform #resetLangCta").text(myResetLangTextCta);
   3462     //$("#resetLangButtonYes").attr("data-t", window.polyglot.t(myResetLangButtonYes));
   3463 
   3464     // Text: <div data-t="nav.home"></div>
   3465     // HTML: <div data-t="privacy" data-t-html></html>
   3466     this.find('[data-t]').each(function() {
   3467       var el = $(this);
   3468       var data = el.data();
   3469       if (data.t) {
   3470         el[data.tHtml === '' ? 'html' : 'text'](polyglot.t(data.t));
   3471       }
   3472     });
   3473 
   3474     return this;
   3475   };
   3476 })();
   3477 /* ##########     END LOCALIZATION     ############ */
   3478 
   3479 // Translations. These should eventually be moved into language-specific files and loaded on demand.
   3480 // jshint nonbsp:false
   3481 switch (window.getLangPref()) {
   3482   case 'ar':
   3483     window.polyglot.extend({
   3484       'newsletter': {
   3485         'title': 'Google Play.          Android   ' +
   3486           '   ',
   3487         'requiredHint': '*  ',
   3488         'name': '.   ',
   3489         'email': '.    ',
   3490         'company': '.   /   ',
   3491         'appUrl': '.   URL    Play',
   3492         'business': {
   3493           'label': '.         ',
   3494           'apps': '',
   3495           'games': '',
   3496           'both': ' '
   3497         },
   3498         'confirmMailingList': '          ' +
   3499           '        Google Play.',
   3500         'privacyPolicy': '          ' +
   3501           '<a href="https://www.google.com/intl/ar/policies/privacy/" target="_blank">Google</a>.',
   3502         'languageVal': 'Arabic ()',
   3503         'successTitle': '!',
   3504         'successDetails': '           Android.'
   3505       }
   3506     });
   3507     break;
   3508   case 'zh-cn':
   3509     window.polyglot.extend({
   3510       'newsletter': {
   3511         'title': ' Android  Google Play ',
   3512         'requiredHint': '* ',
   3513         'name': '',
   3514         'email': '',
   3515         'company': '/',
   3516         'appUrl': ' Play ',
   3517         'business': {
   3518           'label': '',
   3519           'apps': '',
   3520           'games': '',
   3521           'both': ''
   3522         },
   3523         'confirmMailingList': ' Google Play ',
   3524         'privacyPolicy': ' <a href="https://www.google.com/intl/zh-CN/' +
   3525         'policies/privacy/" target="_blank">Google</a> ',
   3526         'languageVal': 'Simplified Chinese ()',
   3527         'successTitle': '',
   3528         'successDetails': ' Android '
   3529       }
   3530     });
   3531     break;
   3532   case 'zh-tw':
   3533     window.polyglot.extend({
   3534       'newsletter': {
   3535         'title': ' Android  Google Play ',
   3536         'requiredHint': '* ',
   3537         'name': '',
   3538         'email': '',
   3539         'company': '/',
   3540         'appUrl': ' Play ',
   3541         'business': {
   3542           'label': '',
   3543           'apps': '',
   3544           'games': '',
   3545           'both': ''
   3546         },
   3547         'confirmMailingList': ' Google Play  Google Play ' +
   3548           ' Google Play ',
   3549         'privacyPolicy': ' <a href="' +
   3550         'https://www.google.com/intl/zh-TW/policies/privacy/" target="_blank">Google</a> .',
   3551         'languageVal': 'Traditional Chinese ()',
   3552         'successTitle': '',
   3553         'successDetails': ' Android '
   3554       }
   3555     });
   3556     break;
   3557   case 'fr':
   3558     window.polyglot.extend({
   3559       'newsletter': {
   3560         'title': 'Recevez les dernires actualits destines aux dveloppeurs Android, ainsi que des conseils qui ' +
   3561           'vous mneront vers le succs sur Google Play.',
   3562         'requiredHint': '* Champs obligatoires',
   3563         'name': 'Nom complet',
   3564         'email': 'Adresse e-mail',
   3565         'company': 'Nom de la socit ou du dveloppeur',
   3566         'appUrl': 'Une de vos URL Play Store',
   3567         'business': {
   3568           'label': 'Quelle option dcrit le mieux votre activit ?',
   3569           'apps': 'Applications',
   3570           'games': 'Jeux',
   3571           'both': 'Applications et jeux'
   3572         },
   3573         'confirmMailingList': 'Ajoutez-moi  la liste de diffusion de la newsletter mensuelle et tenez-moi inform ' +
   3574           'par des e-mails occasionnels de l\'volution et des opportunits de Google Play.',
   3575         'privacyPolicy': 'Je comprends que les renseignements fournis dans ce formulaire seront soumis aux <a href="' +
   3576         'https://www.google.com/intl/fr/policies/privacy/" target="_blank">rgles de confidentialit</a> de Google.',
   3577         'languageVal': 'French (franais)',
   3578         'successTitle': 'Super !',
   3579         'successDetails': 'Vous tes bien inscrit pour recevoir les actualits et les conseils destins aux ' +
   3580           'dveloppeurs Android.'
   3581       }
   3582     });
   3583     break;
   3584   case 'de':
   3585     window.polyglot.extend({
   3586       'newsletter': {
   3587         'title': 'Abonniere aktuelle Informationen und Tipps fr Android-Entwickler und werde noch erfolgreicher ' +
   3588           'bei Google Play.',
   3589         'requiredHint': '* Pflichtfelder',
   3590         'name': 'Vollstndiger Name',
   3591         'email': 'E-Mail-Adresse',
   3592         'company': 'Unternehmens-/Entwicklername',
   3593         'appUrl': 'Eine der URLs deiner Play Store App',
   3594         'business': {
   3595           'label': 'Welche der folgenden Kategorien beschreibt dein Unternehmen am besten?',
   3596           'apps': 'Apps',
   3597           'games': 'Spiele',
   3598           'both': 'Apps und Spiele'
   3599         },
   3600         'confirmMailingList': 'Meine E-Mail-Adresse soll zur Mailingliste hinzugefgt werden, damit ich den ' +
   3601           'monatlichen Newsletter sowie gelegentlich E-Mails zu Entwicklungen und Optionen bei Google Play erhalte.',
   3602         'privacyPolicy': 'Ich besttige, dass die in diesem Formular bereitgestellten Informationen gem der ' +
   3603           '<a href="https://www.google.com/intl/de/policies/privacy/" target="_blank">Datenschutzerklrung</a> von ' +
   3604           'Google verwendet werden drfen.',
   3605         'languageVal': 'German (Deutsch)',
   3606         'successTitle': 'Super!',
   3607         'successDetails': 'Du hast dich erfolgreich angemeldet und erhltst jetzt aktuelle Informationen und Tipps ' +
   3608           'fr Android-Entwickler.'
   3609       }
   3610     });
   3611     break;
   3612   case 'id':
   3613     window.polyglot.extend({
   3614       'newsletter': {
   3615         'title': 'Receba as dicas e as notcias mais recentes para os desenvolvedores Android e seja bem-sucedido ' +
   3616         'no Google Play.',
   3617         'requiredHint': '* Bidang Wajib Diisi',
   3618         'name': 'Nama lengkap',
   3619         'email': 'Alamat email',
   3620         'company': 'Nama pengembang / perusahaan',
   3621         'appUrl': 'Salah satu URL aplikasi Play Store Anda',
   3622         'business': {
   3623           'label': 'Dari berikut ini, mana yang paling cocok dengan bisnis Anda?',
   3624           'apps': 'Aplikasi',
   3625           'games': 'Game',
   3626           'both': 'Aplikasi dan Game'
   3627         },
   3628         'confirmMailingList': 'Tambahkan saya ke milis untuk mendapatkan buletin bulanan dan email sesekali mengenai ' +
   3629           'perkembangan dan kesempatan yang ada di Google Play.',
   3630         'privacyPolicy': 'Saya memahami bahwa informasi yang diberikan dalam formulir ini tunduk pada <a href="' +
   3631         'https://www.google.com/intl/in/policies/privacy/" target="_blank">kebijakan privasi</a> Google.',
   3632         'languageVal': 'Indonesian (Bahasa)',
   3633         'successTitle': 'Hore!',
   3634         'successDetails': 'Anda berhasil mendaftar untuk kiat dan berita pengembang Android terbaru.'
   3635       }
   3636     });
   3637     break;
   3638   case 'it':
   3639     //window.polyglot.extend({
   3640     //  'newsletter': {
   3641     //    'title': 'Receba as dicas e as notcias mais recentes para os desenvolvedores Android e seja bem-sucedido ' +
   3642     //    'no Google Play.',
   3643     //    'requiredHint': '* Campos obrigatrios',
   3644     //    'name': 'Nome completo',
   3645     //    'email': 'Endereo de Email',
   3646     //    'company': 'Nome da empresa / do desenvolvedor',
   3647     //    'appUrl': 'URL de um dos seus apps da Play Store',
   3648     //    'business': {
   3649     //      'label': 'Qual das seguintes opes melhor descreve sua empresa?',
   3650     //      'apps': 'Apps',
   3651     //      'games': 'Jogos',
   3652     //      'both': 'Apps e Jogos'
   3653     //    },
   3654     //    'confirmMailingList': 'Inscreva-me na lista de e-mails para que eu receba o boletim informativo mensal, ' +
   3655     //    'bem como e-mails ocasionais sobre o desenvolvimento e as oportunidades do Google Play.',
   3656     //    'privacyPolicy': 'Reconheo que as informaes fornecidas neste formulrio esto sujeitas  <a href="' +
   3657     //    'https://www.google.com.br/policies/privacy/" target="_blank">Poltica de Privacidade</a> do Google.',
   3658     //    'languageVal': 'Italian (italiano)',
   3659     //    'successTitle': 'Uhu!',
   3660     //    'successDetails': 'Voc se inscreveu para receber as notcias e as dicas mais recentes para os ' +
   3661     //    'desenvolvedores Android.',
   3662     //  }
   3663     //});
   3664     break;
   3665   case 'ja':
   3666     window.polyglot.extend({
   3667       'newsletter': {
   3668         'title': 'Google Play  Android ',
   3669         'requiredHint': '* ',
   3670         'name': '',
   3671         'email': '',
   3672         'company': ' / ',
   3673         'appUrl': 'Play   URL 1 ',
   3674         'business': {
   3675           'label': '',
   3676           'apps': '',
   3677           'games': '',
   3678           'both': ''
   3679         },
   3680         'confirmMailingList': ' Google Play ',
   3681         'privacyPolicy': ' <a href="https://www.google.com/intl/ja/policies/privacy/" ' +
   3682           'target="_blank">Google</a>  ',
   3683         'languageVal': 'Japanese ()',
   3684         'successTitle': '',
   3685         'successDetails': 'Android '
   3686       }
   3687     });
   3688     break;
   3689   case 'ko':
   3690     window.polyglot.extend({
   3691       'newsletter': {
   3692         'title': 'Google Play       Android      .',
   3693         'requiredHint': '*  ',
   3694         'name': '',
   3695         'email': ' ',
   3696         'company': '/ ',
   3697         'appUrl': 'Play   URL  1',
   3698         'business': {
   3699           'label': '        ?',
   3700           'apps': '',
   3701           'games': '',
   3702           'both': '  '
   3703         },
   3704         'confirmMailingList': '  Google Play         .',
   3705         'privacyPolicy': '    <a href="https://www.google.com/intl/ko/policies/privacy/" ' +
   3706           'target="_blank">Google</a>   ',
   3707         'languageVal':'Korean ()',
   3708         'successTitle': '!',
   3709         'successDetails': ' Android         .'
   3710       }
   3711     });
   3712     break;
   3713   case 'pt-br':
   3714     window.polyglot.extend({
   3715       'newsletter': {
   3716         'title': 'Receba as dicas e as notcias mais recentes para os desenvolvedores Android e seja bem-sucedido ' +
   3717         'no Google Play.',
   3718         'requiredHint': '* Campos obrigatrios',
   3719         'name': 'Nome completo',
   3720         'email': 'Endereo de Email',
   3721         'company': 'Nome da empresa / do desenvolvedor',
   3722         'appUrl': 'URL de um dos seus apps da Play Store',
   3723         'business': {
   3724           'label': 'Qual das seguintes opes melhor descreve sua empresa?',
   3725           'apps': 'Apps',
   3726           'games': 'Jogos',
   3727           'both': 'Apps e Jogos'
   3728         },
   3729         'confirmMailingList': 'Inscreva-me na lista de e-mails para que eu receba o boletim informativo mensal, ' +
   3730         'bem como e-mails ocasionais sobre o desenvolvimento e as oportunidades do Google Play.',
   3731         'privacyPolicy': 'Reconheo que as informaes fornecidas neste formulrio esto sujeitas  <a href="' +
   3732         'https://www.google.com.br/policies/privacy/" target="_blank">Poltica de Privacidade</a> do Google.',
   3733         'languageVal': 'Brazilian Portuguese (Portugus Brasileiro)',
   3734         'successTitle': 'Uhu!',
   3735         'successDetails': 'Voc se inscreveu para receber as notcias e as dicas mais recentes para os ' +
   3736         'desenvolvedores Android.'
   3737       }
   3738     });
   3739     break;
   3740   case 'ru':
   3741     window.polyglot.extend({
   3742       'newsletter': {
   3743         'title': '        GooglePlay?   .',
   3744         'requiredHint': '*  ',
   3745         'name': ' ',
   3746         'email': '  ',
   3747         'company': '    ',
   3748         'appUrl': '      GooglePlay',
   3749         'business': {
   3750           'label': '  ?',
   3751           'apps': '',
   3752           'games': '',
   3753           'both': '  '
   3754         },
   3755         'confirmMailingList': '           ' +
   3756           'GooglePlay.',
   3757         'privacyPolicy': '       <a href="' +
   3758           'https://www.google.com/intl/ru/policies/privacy/" target="_blank"> </a> Google.',
   3759         'languageVal': 'Russian ()',
   3760         'successTitle': '!',
   3761         'successDetails': '          Android.'
   3762       }
   3763     });
   3764     break;
   3765   case 'es':
   3766     window.polyglot.extend({
   3767       'newsletter': {
   3768         'title': 'Recibe las ltimas noticias y sugerencias para programadores de Android y logra tener xito en ' +
   3769           'Google Play.',
   3770         'requiredHint': '* Campos obligatorios',
   3771         'name': 'Direccin de correo electrnico',
   3772         'email': 'Endereo de Email',
   3773         'company': 'Nombre de la empresa o del programador',
   3774         'appUrl': 'URL de una de tus aplicaciones de Play Store',
   3775         'business': {
   3776           'label': 'Qu describe mejor a tu empresa?',
   3777           'apps': 'Aplicaciones',
   3778           'games': 'Juegos',
   3779           'both': 'Juegos y aplicaciones'
   3780         },
   3781         'confirmMailingList': 'Deseo unirme a la lista de distribucin para recibir el boletn informativo mensual ' +
   3782           'y correos electrnicos ocasionales sobre desarrollo y oportunidades de Google Play.',
   3783         'privacyPolicy': 'Acepto que la informacin que proporcion en este formulario cumple con la <a href="' +
   3784         'https://www.google.com/intl/es/policies/privacy/" target="_blank">poltica de privacidad</a> de Google.',
   3785         'languageVal': 'Spanish (espaol)',
   3786         'successTitle': 'Felicitaciones!',
   3787         'successDetails': 'El registro para recibir las ltimas noticias y sugerencias para programadores de Android ' +
   3788           'se realiz correctamente.'
   3789       }
   3790     });
   3791     break;
   3792   case 'th':
   3793     window.polyglot.extend({
   3794       'newsletter': {
   3795         'title': ' Android  ' +
   3796           'Google Play',
   3797         'requiredHint': '* ',
   3798         'name': '',
   3799         'email': '',
   3800         'company': '/',
   3801         'appUrl': 'URL  Play ',
   3802         'business': {
   3803           'label': '',
   3804           'apps': '',
   3805           'games': '',
   3806           'both': ''
   3807         },
   3808         'confirmMailingList': '' +
   3809           ' Google Play',
   3810         'privacyPolicy': ' ' +
   3811           '<a href="https://www.google.com/intl/th/policies/privacy/" target="_blank">Google</a>',
   3812         'languageVal': 'Thai ()',
   3813         'successTitle': '!',
   3814         'successDetails': ' Android '
   3815       }
   3816     });
   3817     break;
   3818   case 'tr':
   3819     window.polyglot.extend({
   3820       'newsletter': {
   3821         'title': 'Google Play\'de baarl olmanza yardmc olacak en son Android gelitirici haberleri ve ipular.',
   3822         'requiredHint': '* Zorunlu Alanlar',
   3823         'name': 'Tam ad',
   3824         'email': 'E-posta adresi',
   3825         'company': 'irket / gelitirici ad',
   3826         'appUrl': 'Play Store uygulama URL\'lerinizden biri',
   3827         'business': {
   3828           'label': 'letmenizi en iyi hangisi tanmlar?',
   3829           'apps': 'Uygulamalar',
   3830           'games': 'Oyunlar',
   3831           'both': 'Uygulamalar ve Oyunlar'
   3832         },
   3833         'confirmMailingList': 'Beni, gelitirme ve Google Play frsatlaryla ilgili ara sra gnderilen e-posta ' +
   3834           'iletilerine ilikin posta listesine ve aylk haber bltenine ekle.',
   3835         'privacyPolicy': 'Bu formda salanan bilgilerin Google\'n ' +
   3836           '<a href="https://www.google.com/intl/tr/policies/privacy/" target="_blank">Gizlilik Politikas\'na</a> ' +
   3837           'tabi olacan kabul ediyorum.',
   3838         'languageVal': 'Turkish (Trke)',
   3839         'successTitle': 'Yaasn!',
   3840         'successDetails': 'En son Android gelitirici haberleri ve ipularna baaryla kaydoldunuz.'
   3841       }
   3842     });
   3843     break;
   3844   case 'vi':
   3845     window.polyglot.extend({
   3846       'newsletter': {
   3847         'title': 'Nhn tin tc v mo mi nht dnh cho nh pht trin Android s gip bn tm thy thnh cng trn ' +
   3848           'Google Play.',
   3849         'requiredHint': '* Cc trng bt buc',
   3850         'name': 'Tn y ',
   3851         'email': 'a ch email',
   3852         'company': 'Tn cng ty/nh pht trin',
   3853         'appUrl': 'Mt trong s cc URL ng dng trn ca hng Play ca bn',
   3854         'business': {
   3855           'label': 'La chn no sau y m t chnh xc nht doanh nghip ca bn?',
   3856           'apps': 'ng dng',
   3857           'games': 'Tr chi',
   3858           'both': 'ng dng v tr chi'
   3859         },
   3860         'confirmMailingList': 'Thm ti vo danh sch gi th cho bn tin hng thng v email nh k v vic pht ' +
   3861           'trin v c hi ca Google Play.',
   3862         'privacyPolicy': 'Ti xc nhn rng thng tin c cung cp trong biu mu ny tun th chnh sch bo mt ' +
   3863           'ca <a href="https://www.google.com/intl/vi/policies/privacy/" target="_blank">Google</a>.',
   3864         'languageVal': 'Vietnamese (ting Vit)',
   3865         'successTitle': 'Tht tuyt!',
   3866         'successDetails': 'Bn  ng k thnh cng nhn tin tc v mo mi nht dnh cho nh pht trin ca Android.'
   3867       }
   3868     });
   3869     break;
   3870 }
   3871 
   3872 (function($) {
   3873   'use strict';
   3874 
   3875   function Modal(el, options) {
   3876     this.el = $(el);
   3877     this.options = $.extend({}, options);
   3878     this.isOpen = false;
   3879 
   3880     this.el.on('click', function(event) {
   3881       if (!$.contains(this.el.find('.dac-modal-window')[0], event.target)) {
   3882         return this.el.trigger('modal-close');
   3883       }
   3884     }.bind(this));
   3885 
   3886     this.el.on('modal-open', this.open_.bind(this));
   3887     this.el.on('modal-close', this.close_.bind(this));
   3888     this.el.on('modal-toggle', this.toggle_.bind(this));
   3889   }
   3890 
   3891   Modal.prototype.toggle_ = function() {
   3892     this.el.trigger('modal-' + (this.isOpen ? 'close' : 'open'));
   3893   };
   3894 
   3895   Modal.prototype.close_ = function() {
   3896     // When closing the modal for Android Studio downloads, reload the page
   3897     // because otherwise we might get stuck with post-download dialog state
   3898     if ($("[data-modal='studio_tos'].dac-active").length) {
   3899       location.reload();
   3900     }
   3901     this.el.removeClass('dac-active');
   3902     $('body').removeClass('dac-modal-open');
   3903     this.isOpen = false;
   3904   };
   3905 
   3906   Modal.prototype.open_ = function() {
   3907     this.el.addClass('dac-active');
   3908     $('body').addClass('dac-modal-open');
   3909     this.isOpen = true;
   3910   };
   3911 
   3912   function onClickToggleModal(event) {
   3913     event.preventDefault();
   3914     var toggle = $(event.currentTarget);
   3915     var options = toggle.data();
   3916     var modal = options.modalToggle ? $('[data-modal="' + options.modalToggle + '"]') :
   3917       toggle.closest('[data-modal]');
   3918     modal.trigger('modal-toggle');
   3919   }
   3920 
   3921   /**
   3922    * jQuery plugin
   3923    * @param  {object} options - Override default options.
   3924    */
   3925   $.fn.dacModal = function(options) {
   3926     return this.each(function() {
   3927       new Modal(this, options);
   3928     });
   3929   };
   3930 
   3931   $.fn.dacToggleModal = function(options) {
   3932     return this.each(function() {
   3933       new ToggleModal(this, options);
   3934     });
   3935   };
   3936 
   3937   /**
   3938    * Data Attribute API
   3939    */
   3940   $(document).on('ready.aranja', function() {
   3941     $('[data-modal]').each(function() {
   3942       $(this).dacModal($(this).data());
   3943     });
   3944 
   3945     $('html').on('click.modal', '[data-modal-toggle]', onClickToggleModal);
   3946 
   3947     // Check if url anchor is targetting a toggle to open the modal.
   3948     if (location.hash) {
   3949       var $elem = $(document.getElementById(location.hash.substr(1)));
   3950       if ($elem.attr('data-modal-toggle')) {
   3951         $elem.trigger('click');
   3952       }
   3953     }
   3954 
   3955     var isTargetLangValid = false;
   3956     $(ANDROID_LANGUAGES).each(function(index, langCode) {
   3957       if (langCode == window.getLangTarget()) {
   3958         isTargetLangValid = true;
   3959         return;
   3960       }
   3961     });
   3962     if (window.getLangTarget() !== window.getLangPref() && isTargetLangValid) {
   3963         $('#langform').trigger('modal-open');
   3964         $("#langform button.yes").attr("onclick","window.changeLangPref('" + window.getLangTarget() + "', true);  return false;");
   3965         $("#langform button.no").attr("onclick","window.changeLangPref('" + window.getLangPref() + "', true); return false;");
   3966     }
   3967 
   3968     /* Check the current API level, but only if we're in the reference */
   3969     if (location.pathname.indexOf('/reference') == 0) {
   3970       // init available apis based on user pref
   3971       changeApiLevel();
   3972     }
   3973   });
   3974 })(jQuery);
   3975 
   3976 /* Fullscreen - Toggle fullscreen mode for reference pages */
   3977 (function($) {
   3978   'use strict';
   3979 
   3980   /**
   3981    * @param {HTMLElement} el - The DOM element.
   3982    * @constructor
   3983    */
   3984   function Fullscreen(el) {
   3985     this.el = $(el);
   3986     this.html = $('html');
   3987     this.icon = this.el.find('.dac-sprite');
   3988     this.isFullscreen = window.readCookie(Fullscreen.COOKIE_) === 'true';
   3989     this.activate_();
   3990     this.el.on('click.dac-fullscreen', this.toggleHandler_.bind(this));
   3991   }
   3992 
   3993   /**
   3994    * Cookie name for storing the state
   3995    * @type {string}
   3996    * @private
   3997    */
   3998   Fullscreen.COOKIE_ = 'fullscreen';
   3999 
   4000   /**
   4001    * Classes to modify the DOM
   4002    * @type {{mode: string, fullscreen: string, fullscreenExit: string}}
   4003    * @private
   4004    */
   4005   Fullscreen.CLASSES_ = {
   4006     mode: 'dac-fullscreen-mode',
   4007     fullscreen: 'dac-fullscreen',
   4008     fullscreenExit: 'dac-fullscreen-exit'
   4009   };
   4010 
   4011   /**
   4012    * Event listener for toggling fullscreen mode
   4013    * @param {MouseEvent} event
   4014    * @private
   4015    */
   4016   Fullscreen.prototype.toggleHandler_ = function(event) {
   4017     event.stopPropagation();
   4018     this.toggle(!this.isFullscreen, true);
   4019   };
   4020 
   4021   /**
   4022    * Change the DOM based on current state.
   4023    * @private
   4024    */
   4025   Fullscreen.prototype.activate_ = function() {
   4026     this.icon.toggleClass(Fullscreen.CLASSES_.fullscreen, !this.isFullscreen);
   4027     this.icon.toggleClass(Fullscreen.CLASSES_.fullscreenExit, this.isFullscreen);
   4028     this.html.toggleClass(Fullscreen.CLASSES_.mode, this.isFullscreen);
   4029   };
   4030 
   4031   /**
   4032    * Toggle fullscreen mode and store the state in a cookie.
   4033    */
   4034   Fullscreen.prototype.toggle = function() {
   4035     this.isFullscreen = !this.isFullscreen;
   4036     window.writeCookie(Fullscreen.COOKIE_, this.isFullscreen, null);
   4037     this.activate_();
   4038   };
   4039 
   4040   /**
   4041    * jQuery plugin
   4042    */
   4043   $.fn.dacFullscreen = function() {
   4044     return this.each(function() {
   4045       new Fullscreen($(this));
   4046     });
   4047   };
   4048 })(jQuery);
   4049 
   4050 (function($) {
   4051   'use strict';
   4052 
   4053   /**
   4054    * @param {HTMLElement} selected - The link that is selected in the nav.
   4055    * @constructor
   4056    */
   4057   function HeaderTabs(selected) {
   4058 
   4059     // Don't highlight any tabs on the index page
   4060     if (location.pathname === '/index.html' || location.pathname === '/') {
   4061       //return;
   4062     }
   4063 
   4064     this.selected = $(selected);
   4065     this.selectedParent = this.selected.closest('.dac-nav-secondary').siblings('a');
   4066     this.links = $('.dac-header-tabs a');
   4067 
   4068     this.selectActiveTab();
   4069   }
   4070 
   4071   HeaderTabs.prototype.selectActiveTab = function() {
   4072     var section = null;
   4073 
   4074     if (this.selectedParent.length) {
   4075       section = this.selectedParent.text();
   4076     } else {
   4077       section = this.selected.text();
   4078     }
   4079 
   4080     if (section) {
   4081       this.links.removeClass('selected');
   4082 
   4083       this.links.filter(function() {
   4084         return $(this).text() === $.trim(section);
   4085       }).addClass('selected');
   4086     }
   4087   };
   4088 
   4089   /**
   4090    * jQuery plugin
   4091    */
   4092   $.fn.dacHeaderTabs = function() {
   4093     return this.each(function() {
   4094       new HeaderTabs(this);
   4095     });
   4096   };
   4097 })(jQuery);
   4098 
   4099 (function($) {
   4100   'use strict';
   4101   var icon = $('<i/>').addClass('dac-sprite dac-nav-forward');
   4102   var config = JSON.parse(window.localStorage.getItem('global-navigation') || '{}');
   4103   var forwardLink = $('<span/>')
   4104     .addClass('dac-nav-link-forward')
   4105     .html(icon)
   4106     .attr('tabindex', 0)
   4107     .on('click keypress', function(e) {
   4108       if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
   4109         swap_(e);
   4110       }
   4111     });
   4112 
   4113   /**
   4114    * @constructor
   4115    */
   4116   function Nav(navigation) {
   4117     $('.dac-nav-list').dacCurrentPage().dacHeaderTabs().dacSidebarToggle($('body'));
   4118 
   4119     navigation.find('[data-reference-tree]').dacReferenceNav();
   4120 
   4121     setupViews_(navigation.children().eq(0).children());
   4122 
   4123     initCollapsedNavs(navigation.find('.dac-nav-sub-slider'));
   4124 
   4125     $('#dac-main-navigation').scrollIntoView('.selected')
   4126   }
   4127 
   4128   function updateStore(icon) {
   4129     var navClass = getCurrentLandingPage_(icon);
   4130     var isExpanded = icon.hasClass('dac-expand-less-black');
   4131     var expandedNavs = config.expanded || [];
   4132     if (isExpanded) {
   4133       expandedNavs.push(navClass);
   4134     } else {
   4135       expandedNavs = expandedNavs.filter(function(item) {
   4136         return item !== navClass;
   4137       });
   4138     }
   4139     config.expanded = expandedNavs;
   4140     window.localStorage.setItem('global-navigation', JSON.stringify(config));
   4141   }
   4142 
   4143   function toggleSubNav_(icon) {
   4144     var isExpanded = icon.hasClass('dac-expand-less-black');
   4145     icon.toggleClass('dac-expand-less-black', !isExpanded);
   4146     icon.toggleClass('dac-expand-more-black', isExpanded);
   4147     icon.data('sub-navigation.dac').slideToggle(200);
   4148 
   4149     updateStore(icon);
   4150   }
   4151 
   4152   function handleSubNavToggle_(event) {
   4153     event.preventDefault();
   4154     var icon = $(event.target);
   4155     toggleSubNav_(icon);
   4156   }
   4157 
   4158   function getCurrentLandingPage_(icon) {
   4159     return icon.closest('li')[0].className.replace('dac-nav-item ', '');
   4160   }
   4161 
   4162   // Setup sub navigation collapse/expand
   4163   function initCollapsedNavs(toggleIcons) {
   4164     toggleIcons.each(setInitiallyActive_($('body')));
   4165     toggleIcons.on('click keypress', function(e) {
   4166       if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
   4167         handleSubNavToggle_(e);
   4168       }
   4169     });
   4170   }
   4171 
   4172   function setInitiallyActive_(body) {
   4173     var expandedNavs = config.expanded || [];
   4174     return function(i, icon) {
   4175       icon = $(icon);
   4176       var subNav = icon.next();
   4177 
   4178       if (!subNav.length) {
   4179         return;
   4180       }
   4181 
   4182       var landingPageClass = getCurrentLandingPage_(icon);
   4183       var expanded = expandedNavs.indexOf(landingPageClass) >= 0;
   4184       landingPageClass = landingPageClass === 'home' ? 'about' : landingPageClass;
   4185 
   4186       if (landingPageClass == 'about' && location.pathname == '/index.html') {
   4187         expanded = true;
   4188       }
   4189 
   4190       // TODO: Should read from localStorage
   4191       var visible = body.hasClass(landingPageClass) || expanded;
   4192 
   4193       icon.data('sub-navigation.dac', subNav);
   4194       icon.toggleClass('dac-expand-less-black', visible);
   4195       icon.toggleClass('dac-expand-more-black', !visible);
   4196       subNav.toggle(visible);
   4197     };
   4198   }
   4199 
   4200   function setupViews_(views) {
   4201     if (views.length === 1) {
   4202       // Active tier 1 nav.
   4203       views.addClass('dac-active');
   4204     } else {
   4205       // Activate back button and tier 2 nav.
   4206       views.slice(0, 2).addClass('dac-active');
   4207       var selectedNav = views.eq(2).find('.selected').after(forwardLink);
   4208       var langAttr = selectedNav.attr(window.getLangPref() + '-lang');
   4209       //form the label from locale attr if possible, else set to selectedNav text value
   4210       if ((typeof langAttr !== typeof undefined &&  langAttr !== false) && (langAttr !== '')) {
   4211         $('.dac-nav-back-title').text(langAttr);
   4212       } else {
   4213         $('.dac-nav-back-title').text(selectedNav.text());
   4214       }
   4215     }
   4216 
   4217     // Navigation should animate.
   4218     setTimeout(function() {
   4219       views.removeClass('dac-no-anim');
   4220     }, 10);
   4221   }
   4222 
   4223   function swap_(event) {
   4224     event.preventDefault();
   4225     $(event.currentTarget).trigger('swap-content');
   4226   }
   4227 
   4228   /**
   4229    * jQuery plugin
   4230    */
   4231   $.fn.dacNav = function() {
   4232     return this.each(function() {
   4233       new Nav($(this));
   4234     });
   4235   };
   4236 })(jQuery);
   4237 
   4238 /* global NAVTREE_DATA */
   4239 (function($) {
   4240   /**
   4241    * Build the reference navigation with namespace dropdowns.
   4242    * @param {jQuery} el - The DOM element.
   4243    */
   4244   function buildReferenceNav(el) {
   4245     var supportLibraryPath = '/reference/android/support/';
   4246     var currPath = location.pathname;
   4247 
   4248     if (currPath.indexOf(supportLibraryPath) > -1) {
   4249       updateSupportLibrariesNav(supportLibraryPath, currPath);
   4250     }
   4251     var namespaceList = el.find('[data-reference-namespaces]');
   4252     var resources = $('[data-reference-resources]').detach();
   4253     var selected = namespaceList.find('.selected');
   4254     resources.appendTo(el);
   4255 
   4256     // Links should be toggleable.
   4257     namespaceList.find('a').addClass('dac-reference-nav-toggle dac-closed');
   4258 
   4259     // Set the path for the navtree data to use.
   4260     var navtree_filepath = getNavtreeFilePath(supportLibraryPath, currPath);
   4261 
   4262     // Load in all resources
   4263     $.getScript(navtree_filepath, function(data, textStatus, xhr) {
   4264       if (xhr.status === 200) {
   4265         namespaceList.on(
   4266             'click', 'a.dac-reference-nav-toggle', toggleResourcesHandler);
   4267       }
   4268     });
   4269 
   4270     // No setup required if no resources are present
   4271     if (!resources.length) {
   4272       return;
   4273     }
   4274 
   4275     // The resources should be a part of selected namespace.
   4276     var overview = addResourcesToView(resources, selected);
   4277 
   4278     // Currently viewing Overview
   4279     if (location.href === overview.attr('href')) {
   4280       overview.parent().addClass('selected');
   4281     }
   4282 
   4283     // Open currently selected resource
   4284     var listsToOpen = selected.children().eq(1);
   4285     listsToOpen = listsToOpen.add(
   4286         listsToOpen.find('.selected').parent()).show();
   4287 
   4288     // Mark dropdowns as open
   4289     listsToOpen.prev().removeClass('dac-closed');
   4290 
   4291     // Scroll into view
   4292     namespaceList.scrollIntoView(selected);
   4293   }
   4294 
   4295   function getNavtreeFilePath(supportLibraryPath, currPath) {
   4296     var navtree_filepath = '';
   4297     var navtree_filename = 'navtree_data.js';
   4298     if (currPath.indexOf(supportLibraryPath + 'test') > -1) {
   4299       navtree_filepath = supportLibraryPath + 'test/' + navtree_filename;
   4300     } else if (currPath.indexOf(supportLibraryPath + 'wearable') > -1) {
   4301       navtree_filepath = supportLibraryPath + 'wearable/' + navtree_filename;
   4302     } else {
   4303       navtree_filepath = '/' + navtree_filename;
   4304     }
   4305     return navtree_filepath;
   4306   }
   4307 
   4308   function updateSupportLibrariesNav(supportLibraryPath, currPath) {
   4309     var navTitle = '';
   4310     if (currPath.indexOf(supportLibraryPath + 'test') > -1) {
   4311       navTitle = 'Test Support APIs';
   4312     } else if (currPath.indexOf(supportLibraryPath + 'wearable') > -1) {
   4313       navTitle = 'Wearable Support APIs';
   4314     }
   4315     $('#api-nav-title').text(navTitle);
   4316     $('#api-level-toggle').hide();
   4317   }
   4318 
   4319   /**
   4320    * Handles the toggling of resources.
   4321    * @param {Event} event
   4322    */
   4323   function toggleResourcesHandler(event) {
   4324     event.preventDefault();
   4325     if (event.type == 'click' || event.type == 'keypress' && event.which == 13) {
   4326       var el = $(this);
   4327       // If resources for given namespace is not present, fetch correct data.
   4328       if (this.tagName === 'A' && !this.hasResources) {
   4329         addResourcesToView(buildResourcesViewForData(getDataForNamespace(el.text())), el.parent());
   4330       }
   4331 
   4332       el.toggleClass('dac-closed').next().slideToggle(200);
   4333     }
   4334   }
   4335 
   4336   /**
   4337    * @param {String} namespace
   4338    * @returns {Array} namespace data
   4339    */
   4340   function getDataForNamespace(namespace) {
   4341     var namespaceData = NAVTREE_DATA.filter(function(data) {
   4342       return data[0] === namespace;
   4343     });
   4344 
   4345     return namespaceData.length ? namespaceData[0][2] : [];
   4346   }
   4347 
   4348   /**
   4349    * Build a list item for a resource
   4350    * @param {Array} resource
   4351    * @returns {String}
   4352    */
   4353   function buildResourceItem(resource) {
   4354     return '<li class="api apilevel-' + resource[3] + '"><a href="/' + resource[1] + '">' + resource[0] + '</a></li>';
   4355   }
   4356 
   4357   /**
   4358    * Build resources list items.
   4359    * @param {Array} resources
   4360    * @returns {String}
   4361    */
   4362   function buildResourceList(resources) {
   4363     return '<li><h2>' + resources[0] + '</h2><ul>' + resources[2].map(buildResourceItem).join('') + '</ul>';
   4364   }
   4365 
   4366   /**
   4367    * Build a resources view
   4368    * @param {Array} data
   4369    * @returns {jQuery} resources in an unordered list.
   4370    */
   4371   function buildResourcesViewForData(data) {
   4372     return $('<ul>' + data.map(buildResourceList).join('') + '</ul>');
   4373   }
   4374 
   4375   /**
   4376    * Add resources to a containing view.
   4377    * @param {jQuery} resources
   4378    * @param {jQuery} view
   4379    * @returns {jQuery} the overview link.
   4380    */
   4381   function addResourcesToView(resources, view) {
   4382     var namespace = view.children().eq(0);
   4383     var overview = $('<a href="' + namespace.attr('href') + '">Overview</a>');
   4384 
   4385     // Mark namespace with content;
   4386     namespace[0].hasResources = true;
   4387 
   4388     // Add correct classes / event listeners to resources.
   4389     resources.prepend($('<li>').html(overview))
   4390       .find('a')
   4391         .addClass('dac-reference-nav-resource')
   4392       .end()
   4393         .find('h2').attr('tabindex', 0)
   4394         .addClass('dac-reference-nav-toggle dac-closed')
   4395         .on('click keypress', toggleResourcesHandler)
   4396       .end()
   4397         .add(resources.find('ul'))
   4398         .addClass('dac-reference-nav-resources')
   4399       .end()
   4400         .appendTo(view);
   4401 
   4402     return overview;
   4403   }
   4404 
   4405   function setActiveReferencePackage(el) {
   4406     var packageLinkEls = el.find('[data-reference-namespaces] a');
   4407     var selected = null;
   4408     var highestMatchCount = 0;
   4409     packageLinkEls.each(function(index, linkEl) {
   4410       var matchCount = 0;
   4411       $(location.pathname.split('/')).each(function(index, subpath) {
   4412         if (linkEl.href.indexOf('/' + subpath + '/') > -1) {
   4413           matchCount++;
   4414         }
   4415       });
   4416       if (matchCount > highestMatchCount) {
   4417         selected = linkEl;
   4418         highestMatchCount = matchCount;
   4419       }
   4420     });
   4421     $(selected).parent().addClass('selected');
   4422   }
   4423 
   4424   /**
   4425    * jQuery plugin
   4426    */
   4427   $.fn.dacReferenceNav = function() {
   4428     return this.each(function() {
   4429       setActiveReferencePackage($(this));
   4430       buildReferenceNav($(this));
   4431     });
   4432   };
   4433 })(jQuery);
   4434 
   4435 /** Scroll a container to make a target element visible
   4436  This is called when the page finished loading. */
   4437 $.fn.scrollIntoView = function(target) {
   4438   if ('string' === typeof target) {
   4439     target = this.find(target);
   4440   }
   4441   if (this.is(':visible')) {
   4442     if (target.length == 0) {
   4443       // If no selected item found, exit
   4444       return;
   4445     }
   4446 
   4447     // get the target element's offset from its container nav by measuring the element's offset
   4448     // relative to the document then subtract the container nav's offset relative to the document
   4449     var targetOffset = target.offset().top - this.offset().top;
   4450     var containerHeight = this.height();
   4451     if (targetOffset > containerHeight * .8) { // multiply nav height by .8 so we move up the item
   4452       // if it's more than 80% down the nav
   4453       // scroll the item up by an amount equal to 80% the container height
   4454       this.scrollTop(targetOffset - (containerHeight * .8));
   4455     }
   4456   }
   4457 };
   4458 
   4459 (function($) {
   4460   $.fn.dacCurrentPage = function() {
   4461     // Highlight the header tabs...
   4462     // highlight Design tab
   4463     var baseurl = getBaseUri(window.location.pathname);
   4464     var urlSegments = baseurl.split('/');
   4465     var navEl = this;
   4466     var body = $('body');
   4467     var subNavEl = navEl.find('.dac-nav-secondary');
   4468     var parentNavEl;
   4469     var selected;
   4470     // In NDK docs, highlight appropriate sub-nav
   4471     if (body.hasClass('dac-ndk')) {
   4472       if (body.hasClass('guide')) {
   4473         selected = navEl.find('> li.guides > a').addClass('selected');
   4474       } else if (body.hasClass('reference')) {
   4475         selected = navEl.find('> li.reference > a').addClass('selected');
   4476       } else if (body.hasClass('samples')) {
   4477         selected = navEl.find('> li.samples > a').addClass('selected');
   4478       } else if (body.hasClass('downloads')) {
   4479         selected = navEl.find('> li.downloads > a').addClass('selected');
   4480       }
   4481     } else if (body.hasClass('dac-studio')) {
   4482       if (body.hasClass('download')) {
   4483         selected = navEl.find('> li.download > a').addClass('selected');
   4484       } else if (body.hasClass('features')) {
   4485         selected = navEl.find('> li.features > a').addClass('selected');
   4486       } else if (body.hasClass('guide')) {
   4487         selected = navEl.find('> li.guide > a').addClass('selected');
   4488       } else if (body.hasClass('preview')) {
   4489         selected = navEl.find('> li.preview > a').addClass('selected');
   4490       }
   4491     } else if (body.hasClass('design')) {
   4492       selected = navEl.find('> li.design > a').addClass('selected');
   4493       // highlight Home nav
   4494     } else if (body.hasClass('about') || location.pathname == '/index.html') {
   4495       parentNavEl = navEl.find('> li.home > a');
   4496       parentNavEl.addClass('has-subnav');
   4497       // In Home docs, also highlight appropriate sub-nav
   4498       if (urlSegments[1] === 'wear' || urlSegments[1] === 'tv' ||
   4499         urlSegments[1] === 'auto') {
   4500         selected = subNavEl.find('li.' + urlSegments[1] + ' > a').addClass('selected');
   4501       } else if (urlSegments[1] === 'about') {
   4502         selected = subNavEl.find('li.versions > a').addClass('selected');
   4503       } else {
   4504         selected = parentNavEl.removeClass('has-subnav').addClass('selected');
   4505       }
   4506       // highlight Develop nav
   4507     } else if (body.hasClass('develop') || body.hasClass('google')) {
   4508       parentNavEl = navEl.find('> li.develop > a');
   4509       parentNavEl.addClass('has-subnav');
   4510       // In Develop docs, also highlight appropriate sub-nav
   4511       if (urlSegments[1] === 'training') {
   4512         selected = subNavEl.find('li.training > a').addClass('selected');
   4513       } else if (urlSegments[1] === 'guide') {
   4514         selected = subNavEl.find('li.guide > a').addClass('selected');
   4515       } else if (urlSegments[1] === 'reference') {
   4516         // If the root is reference, but page is also part of Google Services, select Google
   4517         if (body.hasClass('google')) {
   4518           selected = subNavEl.find('li.google > a').addClass('selected');
   4519         } else {
   4520           selected = subNavEl.find('li.reference > a').addClass('selected');
   4521         }
   4522       } else if (body.hasClass('google')) {
   4523         selected = subNavEl.find('li.google > a').addClass('selected');
   4524       } else if (body.hasClass('samples')) {
   4525         selected = subNavEl.find('li.samples > a').addClass('selected');
   4526       } else {
   4527         selected = parentNavEl.removeClass('has-subnav').addClass('selected');
   4528       }
   4529       // highlight Distribute nav
   4530     } else if (body.hasClass('distribute')) {
   4531       parentNavEl = navEl.find('> li.distribute > a');
   4532       parentNavEl.addClass('has-subnav');
   4533       // In Distribute docs, also highlight appropriate sub-nav
   4534       if (urlSegments[2] === 'users') {
   4535         selected = subNavEl.find('li.users > a').addClass('selected');
   4536       } else if (urlSegments[2] === 'engage') {
   4537         selected = subNavEl.find('li.engage > a').addClass('selected');
   4538       } else if (urlSegments[2] === 'monetize') {
   4539         selected = subNavEl.find('li.monetize > a').addClass('selected');
   4540       } else if (urlSegments[2] === 'analyze') {
   4541         selected = subNavEl.find('li.analyze > a').addClass('selected');
   4542       } else if (urlSegments[2] === 'tools') {
   4543         selected = subNavEl.find('li.disttools > a').addClass('selected');
   4544       } else if (urlSegments[2] === 'stories') {
   4545         selected = subNavEl.find('li.stories > a').addClass('selected');
   4546       } else if (urlSegments[2] === 'essentials') {
   4547         selected = subNavEl.find('li.essentials > a').addClass('selected');
   4548       } else if (urlSegments[2] === 'googleplay') {
   4549         selected = subNavEl.find('li.googleplay > a').addClass('selected');
   4550       } else {
   4551         selected = parentNavEl.removeClass('has-subnav').addClass('selected');
   4552       }
   4553     } else if (body.hasClass('preview')) {
   4554       selected = navEl.find('> li.preview > a').addClass('selected');
   4555     }
   4556     return $(selected);
   4557   };
   4558 })(jQuery);
   4559 
   4560 (function($) {
   4561   'use strict';
   4562 
   4563   /**
   4564    * Toggle the visabilty of the mobile navigation.
   4565    * @param {HTMLElement} el - The DOM element.
   4566    * @param {Object} options
   4567    * @constructor
   4568    */
   4569   function ToggleNav(el, options) {
   4570     this.el = $(el);
   4571     this.options = $.extend({}, ToggleNav.DEFAULTS_, options);
   4572     this.body = $(document.body);
   4573     this.navigation_ = this.body.find(this.options.navigation);
   4574     this.el.on('click', this.clickHandler_.bind(this));
   4575   }
   4576 
   4577   ToggleNav.BREAKPOINT_ = 980;
   4578 
   4579   /**
   4580    * Open on correct sizes
   4581    */
   4582   function toggleSidebarVisibility(body) {
   4583     var wasClosed = ('' + localStorage.getItem('navigation-open')) === 'false';
   4584     // Override the local storage setting for navigation-open for child sites
   4585     // with no-subnav class.
   4586     if (document.body.classList.contains('no-subnav')) {
   4587       wasClosed = false;
   4588     }
   4589 
   4590     if (wasClosed) {
   4591       body.removeClass(ToggleNav.DEFAULTS_.activeClass);
   4592     } else if (window.innerWidth >= ToggleNav.BREAKPOINT_) {
   4593       body.addClass(ToggleNav.DEFAULTS_.activeClass);
   4594     } else {
   4595       body.removeClass(ToggleNav.DEFAULTS_.activeClass);
   4596     }
   4597   }
   4598 
   4599   /**
   4600    * ToggleNav Default Settings
   4601    * @type {{body: boolean, dimmer: string, navigation: string, activeClass: string}}
   4602    * @private
   4603    */
   4604   ToggleNav.DEFAULTS_ = {
   4605     body: true,
   4606     dimmer: '.dac-nav-dimmer',
   4607     animatingClass: 'dac-nav-animating',
   4608     navigation: '[data-dac-nav]',
   4609     activeClass: 'dac-nav-open'
   4610   };
   4611 
   4612   /**
   4613    * The actual toggle logic.
   4614    * @param {Event} event
   4615    * @private
   4616    */
   4617   ToggleNav.prototype.clickHandler_ = function(event) {
   4618     event.preventDefault();
   4619     var animatingClass = this.options.animatingClass;
   4620     var body = this.body;
   4621 
   4622     body.addClass(animatingClass);
   4623     body.toggleClass(this.options.activeClass);
   4624 
   4625     setTimeout(function() {
   4626       body.removeClass(animatingClass);
   4627     }, this.navigation_.transitionDuration());
   4628 
   4629     if (window.innerWidth >= ToggleNav.BREAKPOINT_) {
   4630       localStorage.setItem('navigation-open', body.hasClass(this.options.activeClass));
   4631     }
   4632   };
   4633 
   4634   /**
   4635    * jQuery plugin
   4636    * @param  {object} options - Override default options.
   4637    */
   4638   $.fn.dacToggleMobileNav = function() {
   4639     return this.each(function() {
   4640       var el = $(this);
   4641       new ToggleNav(el, el.data());
   4642     });
   4643   };
   4644 
   4645   $.fn.dacSidebarToggle = function(body) {
   4646     toggleSidebarVisibility(body);
   4647     $(window).on('resize', toggleSidebarVisibility.bind(null, body));
   4648   };
   4649 
   4650   /**
   4651    * Data Attribute API
   4652    */
   4653   $(function() {
   4654     $('[data-dac-toggle-nav]').dacToggleMobileNav();
   4655   });
   4656 })(jQuery);
   4657 
   4658 (function($) {
   4659   'use strict';
   4660 
   4661   /**
   4662    * Submit the newsletter form to a Google Form.
   4663    * @param {HTMLElement} el - The Form DOM element.
   4664    * @constructor
   4665    */
   4666   function NewsletterForm(el) {
   4667     this.el = $(el);
   4668     this.form = this.el.find('form');
   4669     $('<iframe/>').hide()
   4670       .attr('name', 'dac-newsletter-iframe')
   4671       .attr('src', '')
   4672       .insertBefore(this.form);
   4673     this.el.find('[data-newsletter-language]').val(window.polyglot.t('newsletter.languageVal'));
   4674     this.form.on('submit', this.submitHandler_.bind(this));
   4675   }
   4676 
   4677   /**
   4678    * Milliseconds until modal has vanished after modal-close is triggered.
   4679    * @type {number}
   4680    * @private
   4681    */
   4682   NewsletterForm.CLOSE_DELAY_ = 300;
   4683 
   4684   /**
   4685    * Switch view to display form after close.
   4686    * @private
   4687    */
   4688   NewsletterForm.prototype.closeHandler_ = function() {
   4689     setTimeout(function() {
   4690       this.el.trigger('swap-reset');
   4691     }.bind(this), NewsletterForm.CLOSE_DELAY_);
   4692   };
   4693 
   4694   /**
   4695    * Reset the modal to initial state.
   4696    * @private
   4697    */
   4698   NewsletterForm.prototype.reset_ = function() {
   4699     this.form.trigger('reset');
   4700     this.el.one('modal-close', this.closeHandler_.bind(this));
   4701   };
   4702 
   4703   /**
   4704    * Display a success view on submit.
   4705    * @private
   4706    */
   4707   NewsletterForm.prototype.submitHandler_ = function() {
   4708     this.el.one('swap-complete', this.reset_.bind(this));
   4709     this.el.trigger('swap-content');
   4710   };
   4711 
   4712   /**
   4713    * jQuery plugin
   4714    * @param  {object} options - Override default options.
   4715    */
   4716   $.fn.dacNewsletterForm = function(options) {
   4717     return this.each(function() {
   4718       new NewsletterForm(this, options);
   4719     });
   4720   };
   4721 
   4722   /**
   4723    * Data Attribute API
   4724    */
   4725   $(document).on('ready.aranja', function() {
   4726     $('[data-newsletter]').each(function() {
   4727       $(this).dacNewsletterForm();
   4728     });
   4729   });
   4730 })(jQuery);
   4731 
   4732 /* globals METADATA, YOUTUBE_RESOURCES, BLOGGER_RESOURCES */
   4733 window.metadata = {};
   4734 
   4735 /**
   4736  * Prepare metadata and indices for querying.
   4737  */
   4738 window.metadata.prepare = (function() {
   4739   // Helper functions.
   4740   function mergeArrays() {
   4741     return Array.prototype.concat.apply([], arguments);
   4742   }
   4743 
   4744   /**
   4745    * Creates lookup maps for a resource index.
   4746    * I.e. where MAP['some tag'][resource.id] === true when that resource has 'some tag'.
   4747    * @param resourceDict
   4748    * @returns {{}}
   4749    */
   4750   function buildResourceLookupMap(resourceDict) {
   4751     var map = {};
   4752     for (var key in resourceDict) {
   4753       var dictForKey = {};
   4754       var srcArr = resourceDict[key];
   4755       for (var i = 0; i < srcArr.length; i++) {
   4756         dictForKey[srcArr[i].index] = true;
   4757       }
   4758       map[key] = dictForKey;
   4759     }
   4760     return map;
   4761   }
   4762 
   4763   /**
   4764    * Merges metadata maps for english and the current language into the global store.
   4765    */
   4766   function mergeMetadataMap(name, locale) {
   4767     if (locale && locale !== 'en' && METADATA[locale]) {
   4768       METADATA[name] = $.extend(METADATA.en[name], METADATA[locale][name]);
   4769     } else {
   4770       METADATA[name] = METADATA.en[name];
   4771     }
   4772   }
   4773 
   4774   /**
   4775    * Index all resources by type, url, tag and category.
   4776    * @param resources
   4777    */
   4778   function createIndices(resources) {
   4779     // URL, type, tag and category lookups
   4780     var byType = METADATA.byType = {};
   4781     var byUrl = METADATA.byUrl = {};
   4782     var byTag = METADATA.byTag = {};
   4783     var byCategory = METADATA.byCategory = {};
   4784 
   4785     for (var i = 0; i < resources.length; i++) {
   4786       var res = resources[i];
   4787 
   4788       // Store index.
   4789       res.index = i;
   4790 
   4791       // Index by type.
   4792       var type = res.type;
   4793       if (type) {
   4794         byType[type] = byType[type] || [];
   4795         byType[type].push(res);
   4796       }
   4797 
   4798       // Index by tag.
   4799       var tags = res.tags || [];
   4800       for (var j = 0; j < tags.length; j++) {
   4801         var tag = tags[j];
   4802         if (tag) {
   4803           byTag[tag] = byTag[tag] || [];
   4804           byTag[tag].push(res);
   4805         }
   4806       }
   4807 
   4808       // Index by category.
   4809       var category = res.category;
   4810       if (category) {
   4811         byCategory[category] = byCategory[category] || [];
   4812         byCategory[category].push(res);
   4813       }
   4814 
   4815       // Index by url.
   4816       var url = res.url;
   4817       if (url) {
   4818         res.baseUrl = url.replace(/^intl\/\w+[\/]/, '');
   4819         byUrl[res.baseUrl] = res;
   4820       }
   4821     }
   4822     METADATA.hasType = buildResourceLookupMap(byType);
   4823     METADATA.hasTag = buildResourceLookupMap(byTag);
   4824     METADATA.hasCategory = buildResourceLookupMap(byCategory);
   4825   }
   4826 
   4827   return function() {
   4828     // Only once.
   4829     if (METADATA.all) { return; }
   4830 
   4831     // Get current language.
   4832     var locale = getLangPref();
   4833     // Merge english resources.
   4834     if (useDevsiteMetadata) {
   4835       var all_keys = Object.keys(METADATA['en']);
   4836       METADATA.all = []
   4837 
   4838       $(all_keys).each(function(index, category) {
   4839         if (RESERVED_METADATA_CATEGORY_NAMES.indexOf(category) == -1) {
   4840           METADATA.all = mergeArrays(
   4841             METADATA.all,
   4842             METADATA.en[category]
   4843           );
   4844         }
   4845       });
   4846 
   4847       METADATA.all = mergeArrays(
   4848         METADATA.all,
   4849         YOUTUBE_RESOURCES,
   4850         BLOGGER_RESOURCES,
   4851         METADATA.en.extras
   4852       );
   4853     } else {
   4854       METADATA.all = mergeArrays(
   4855         METADATA.en.about,
   4856         METADATA.en.design,
   4857         METADATA.en.distribute,
   4858         METADATA.en.develop,
   4859         YOUTUBE_RESOURCES,
   4860         BLOGGER_RESOURCES,
   4861         METADATA.en.extras
   4862       );
   4863     }
   4864 
   4865     // Merge local language resources.
   4866     if (locale !== 'en' && METADATA[locale]) {
   4867       if (useDevsiteMetadata) {
   4868         all_keys = Object.keys(METADATA[locale]);
   4869         $(all_keys).each(function(index, category) {
   4870           if (RESERVED_METADATA_CATEGORY_NAMES.indexOf(category) == -1) {
   4871             METADATA.all = mergeArrays(
   4872               METADATA.all,
   4873               METADATA.en[category]
   4874             );
   4875           }
   4876         });
   4877 
   4878         METADATA.all = mergeArrays(
   4879           METADATA.all,
   4880           METADATA[locale].extras
   4881         );
   4882       } else {
   4883         METADATA.all = mergeArrays(
   4884           METADATA.all,
   4885           METADATA[locale].about,
   4886           METADATA[locale].design,
   4887           METADATA[locale].distribute,
   4888           METADATA[locale].develop,
   4889           METADATA[locale].extras
   4890         );
   4891 
   4892       }
   4893     }
   4894 
   4895     mergeMetadataMap('collections', locale);
   4896     mergeMetadataMap('searchHeroCollections', locale);
   4897     mergeMetadataMap('carousel', locale);
   4898 
   4899     // Create query indicies for resources.
   4900     createIndices(METADATA.all, locale);
   4901 
   4902     // Reference metadata.
   4903     METADATA.androidReference = mergeArrays(
   4904         window.DATA, window.SUPPORT_WEARABLE_DATA, window.SUPPORT_TEST_DATA);
   4905     METADATA.googleReference = mergeArrays(window.GMS_DATA, window.GCM_DATA);
   4906   };
   4907 })();
   4908 
   4909 /* global METADATA, util */
   4910 window.metadata.query = (function($) {
   4911   var pageMap = {};
   4912 
   4913   function buildResourceList(opts) {
   4914     window.metadata.prepare();
   4915     var expressions = parseResourceQuery(opts.query || '');
   4916     var instanceMap = {};
   4917     var results = [];
   4918 
   4919     for (var i = 0; i < expressions.length; i++) {
   4920       var clauses = expressions[i];
   4921 
   4922       // Get all resources for first clause
   4923       var resources = getResourcesForClause(clauses.shift());
   4924 
   4925       // Concat to final results list
   4926       results = results.concat(resources.map(filterResources(clauses, i > 0, instanceMap)).filter(filterEmpty));
   4927     }
   4928 
   4929     // Set correct order
   4930     if (opts.sortOrder && results.length) {
   4931       results = opts.sortOrder === 'random' ? util.shuffle(results) : results.sort(sortResultsByKey(opts.sortOrder));
   4932     }
   4933 
   4934     // Slice max results.
   4935     if (opts.maxResults !== Infinity) {
   4936       results = results.slice(0, opts.maxResults);
   4937     }
   4938 
   4939     // Remove page level duplicates
   4940     if (opts.allowDuplicates === undefined || opts.allowDuplicates === 'false') {
   4941       results = results.filter(removePageLevelDuplicates);
   4942 
   4943       for (var index = 0; index < results.length; ++index) {
   4944         pageMap[results[index].index] = 1;
   4945       }
   4946     }
   4947 
   4948     return results;
   4949   }
   4950 
   4951   function filterResources(clauses, removeDuplicates, map) {
   4952     return function(resource) {
   4953       var resourceIsAllowed = true;
   4954 
   4955       // References must be defined.
   4956       if (resource === undefined) {
   4957         return;
   4958       }
   4959 
   4960       // Get canonical (localized) version of resource if possible.
   4961       resource = METADATA.byUrl[resource.baseUrl] || METADATA.byUrl[resource.url] || resource;
   4962 
   4963       // Filter out resources already used
   4964       if (removeDuplicates) {
   4965         resourceIsAllowed = !map[resource.index];
   4966       }
   4967 
   4968       // Must fulfill all criteria
   4969       if (clauses.length > 0) {
   4970         resourceIsAllowed = resourceIsAllowed && doesResourceMatchClauses(resource, clauses);
   4971       }
   4972 
   4973       // Mark resource as used.
   4974       if (resourceIsAllowed) {
   4975         map[resource.index] = 1;
   4976       }
   4977 
   4978       return resourceIsAllowed && resource;
   4979     };
   4980   }
   4981 
   4982   function filterEmpty(resource) {
   4983     return resource;
   4984   }
   4985 
   4986   function sortResultsByKey(key) {
   4987     var desc = key.charAt(0) === '-';
   4988 
   4989     if (desc) {
   4990       key = key.substring(1);
   4991     }
   4992 
   4993     return function(x, y) {
   4994       return (desc ? -1 : 1) * (parseInt(x[key], 10) - parseInt(y[key], 10));
   4995     };
   4996   }
   4997 
   4998   function getResourcesForClause(clause) {
   4999     switch (clause.attr) {
   5000       case 'type':
   5001         return METADATA.byType[clause.value];
   5002       case 'tag':
   5003         return METADATA.byTag[clause.value];
   5004       case 'collection':
   5005         var resources = METADATA.collections[clause.value] || {};
   5006         return getResourcesByUrlCollection(resources.resources);
   5007       case 'history':
   5008         return getResourcesByUrlCollection($.dacGetVisitedUrls(clause.value));
   5009       case 'section':
   5010         return getResourcesByUrlCollection([clause.value].sections);
   5011       default:
   5012         return [];
   5013     }
   5014   }
   5015 
   5016   function getResourcesByUrlCollection(resources) {
   5017     return (resources || []).map(function(url) {
   5018       return METADATA.byUrl[url];
   5019     });
   5020   }
   5021 
   5022   function removePageLevelDuplicates(resource) {
   5023     return resource && !pageMap[resource.index];
   5024   }
   5025 
   5026   function doesResourceMatchClauses(resource, clauses) {
   5027     for (var i = 0; i < clauses.length; i++) {
   5028       var map;
   5029       switch (clauses[i].attr) {
   5030         case 'type':
   5031           map = METADATA.hasType[clauses[i].value];
   5032           break;
   5033         case 'tag':
   5034           map = METADATA.hasTag[clauses[i].value];
   5035           break;
   5036       }
   5037 
   5038       if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) {
   5039         return clauses[i].negative;
   5040       }
   5041     }
   5042 
   5043     return true;
   5044   }
   5045 
   5046   function parseResourceQuery(query) {
   5047     // Parse query into array of expressions (expression e.g. 'tag:foo + type:video')
   5048     var expressions = [];
   5049     var expressionStrs = query.split(',') || [];
   5050     for (var i = 0; i < expressionStrs.length; i++) {
   5051       var expr = expressionStrs[i] || '';
   5052 
   5053       // Break expression into clauses (clause e.g. 'tag:foo')
   5054       var clauses = [];
   5055       var clauseStrs = expr.split(/(?=[\+\-])/);
   5056       for (var j = 0; j < clauseStrs.length; j++) {
   5057         var clauseStr = clauseStrs[j] || '';
   5058 
   5059         // Get attribute and value from clause (e.g. attribute='tag', value='foo')
   5060         var parts = clauseStr.split(':');
   5061         var clause = {};
   5062 
   5063         clause.attr = parts[0].replace(/^\s+|\s+$/g, '');
   5064         if (clause.attr) {
   5065           if (clause.attr.charAt(0) === '+') {
   5066             clause.attr = clause.attr.substring(1);
   5067           } else if (clause.attr.charAt(0) === '-') {
   5068             clause.negative = true;
   5069             clause.attr = clause.attr.substring(1);
   5070           }
   5071         }
   5072 
   5073         if (parts.length > 1) {
   5074           clause.value = parts[1].replace(/^\s+|\s+$/g, '');
   5075         }
   5076 
   5077         clauses.push(clause);
   5078       }
   5079 
   5080       if (!clauses.length) {
   5081         continue;
   5082       }
   5083 
   5084       expressions.push(clauses);
   5085     }
   5086 
   5087     return expressions;
   5088   }
   5089 
   5090   return buildResourceList;
   5091 })(jQuery);
   5092 
   5093 /* global METADATA, getLangPref */
   5094 
   5095 window.metadata.search = (function() {
   5096   'use strict';
   5097 
   5098   var currentLang = getLangPref();
   5099 
   5100   function search(query) {
   5101     window.metadata.prepare();
   5102     return {
   5103       android: findDocsMatches(query, METADATA.androidReference),
   5104       docs: findDocsMatches(query, METADATA.googleReference),
   5105       resources: findResourceMatches(query)
   5106     };
   5107   }
   5108 
   5109   function findDocsMatches(query, data) {
   5110     var results = [];
   5111 
   5112     for (var i = 0; i < data.length; i++) {
   5113       var s = data[i];
   5114       if (query.length !== 0 && s.label.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
   5115         results.push(s);
   5116       }
   5117     }
   5118 
   5119     rankAutocompleteApiResults(query, results);
   5120 
   5121     return results;
   5122   }
   5123 
   5124   function findResourceMatches(query) {
   5125     var results = [];
   5126 
   5127     // Search for matching JD docs
   5128     if (query.length >= 2) {
   5129       /* In some langs, spaces may be optional between certain non-Ascii word-glyphs. For
   5130        * those langs, only match query at word boundaries if query includes Ascii chars only.
   5131        */
   5132       var NO_BOUNDARY_LANGUAGES = ['ja','ko','vi','zh-cn','zh-tw'];
   5133       var isAsciiOnly = /^[\u0000-\u007f]*$/.test(query);
   5134       var noBoundaries = (NO_BOUNDARY_LANGUAGES.indexOf(window.getLangPref()) !== -1);
   5135       var exprBoundary = (!isAsciiOnly && noBoundaries) ? '' : '(?:^|\\s)';
   5136       var queryRegex = new RegExp(exprBoundary + query.toLowerCase(), 'g');
   5137 
   5138       var all = METADATA.all;
   5139       for (var i = 0; i < all.length; i++) {
   5140         // current search comparison, with counters for tag and title,
   5141         // used later to improve ranking
   5142         var s = all[i];
   5143         s.matched_tag = 0;
   5144         s.matched_title = 0;
   5145         var matched = false;
   5146 
   5147         // Check if query matches any tags; work backwards toward 1 to assist ranking
   5148         if (s.keywords) {
   5149           for (var j = s.keywords.length - 1; j >= 0; j--) {
   5150             // it matches a tag
   5151             if (s.keywords[j].toLowerCase().match(queryRegex)) {
   5152               matched = true;
   5153               s.matched_tag = j + 1; // add 1 to index position
   5154             }
   5155           }
   5156         }
   5157 
   5158         // Check if query matches doc title
   5159         if (s.title.toLowerCase().match(queryRegex)) {
   5160           matched = true;
   5161           s.matched_title = 1;
   5162         }
   5163 
   5164         // Remember the doc if it matches either
   5165         if (matched) {
   5166           results.push(s);
   5167         }
   5168       }
   5169 
   5170       // Improve the current results
   5171       results = lookupBetterResult(results);
   5172 
   5173       // Rank/sort all the matched pages
   5174       rankAutocompleteDocResults(results);
   5175 
   5176       return results;
   5177     }
   5178   }
   5179 
   5180   // Replaces a match with another resource by url, if it exists.
   5181   function lookupReplacementByUrl(match, url) {
   5182     var replacement = METADATA.byUrl[url];
   5183 
   5184     // Replacement resource does not exists.
   5185     if (!replacement) { return; }
   5186 
   5187     replacement.matched_title = Math.max(replacement.matched_title, match.matched_title);
   5188     replacement.matched_tag = Math.max(replacement.matched_tag, match.matched_tag);
   5189 
   5190     return replacement;
   5191   }
   5192 
   5193   // Find the localized version of a page if it exists.
   5194   function lookupLocalizedVersion(match) {
   5195     return METADATA.byUrl[match.baseUrl] || METADATA.byUrl[match.url];
   5196   }
   5197 
   5198   // Find the main page for a tutorial when matching a subpage.
   5199   function lookupTutorialIndex(match) {
   5200     // Guard for non index tutorial pages.
   5201     if (match.type !== 'training' || match.url.indexOf('index.html') >= 0) { return; }
   5202 
   5203     var indexUrl = match.url.replace(/[^\/]+$/, 'index.html');
   5204     return lookupReplacementByUrl(match, indexUrl);
   5205   }
   5206 
   5207   // Find related results which are a better match for the user.
   5208   function lookupBetterResult(matches) {
   5209     var newMatches = [];
   5210 
   5211     matches = matches.filter(function(match) {
   5212       var newMatch = match;
   5213       newMatch = lookupTutorialIndex(newMatch) || newMatch;
   5214       newMatch = lookupLocalizedVersion(newMatch) || newMatch;
   5215 
   5216       if (newMatch !== match) {
   5217         newMatches.push(newMatch);
   5218       }
   5219 
   5220       return newMatch === match;
   5221     });
   5222 
   5223     return toUnique(newMatches.concat(matches));
   5224   }
   5225 
   5226   /* Order the jd doc result list based on match quality */
   5227   function rankAutocompleteDocResults(matches) {
   5228     if (!matches || !matches.length) {
   5229       return;
   5230     }
   5231 
   5232     var _resultScoreFn = function(match) {
   5233       var score = 1.0;
   5234 
   5235       // if the query matched a tag
   5236       if (match.matched_tag > 0) {
   5237         // multiply score by factor relative to position in tags list (max of 3)
   5238         score *= 3 / match.matched_tag;
   5239 
   5240         // if it also matched the title
   5241         if (match.matched_title > 0) {
   5242           score *= 2;
   5243         }
   5244       } else if (match.matched_title > 0) {
   5245         score *= 3;
   5246       }
   5247 
   5248       if (match.lang === currentLang) {
   5249         score *= 5;
   5250       }
   5251 
   5252       return score;
   5253     };
   5254 
   5255     for (var i = 0; i < matches.length; i++) {
   5256       matches[i].__resultScore = _resultScoreFn(matches[i]);
   5257     }
   5258 
   5259     matches.sort(function(a, b) {
   5260       var n = b.__resultScore - a.__resultScore;
   5261 
   5262       if (n === 0) {
   5263         // lexicographical sort if scores are the same
   5264         n = (a.title < b.title) ? -1 : 1;
   5265       }
   5266 
   5267       return n;
   5268     });
   5269   }
   5270 
   5271   /* Order the result list based on match quality */
   5272   function rankAutocompleteApiResults(query, matches) {
   5273     query = query || '';
   5274     if (!matches || !matches.length) {
   5275       return;
   5276     }
   5277 
   5278     // helper function that gets the last occurence index of the given regex
   5279     // in the given string, or -1 if not found
   5280     var _lastSearch = function(s, re) {
   5281       if (s === '') {
   5282         return -1;
   5283       }
   5284       var l = -1;
   5285       var tmp;
   5286       while ((tmp = s.search(re)) >= 0) {
   5287         if (l < 0) {
   5288           l = 0;
   5289         }
   5290         l += tmp;
   5291         s = s.substr(tmp + 1);
   5292       }
   5293       return l;
   5294     };
   5295 
   5296     // helper function that counts the occurrences of a given character in
   5297     // a given string
   5298     var _countChar = function(s, c) {
   5299       var n = 0;
   5300       for (var i = 0; i < s.length; i++) {
   5301         if (s.charAt(i) === c) {
   5302           ++n;
   5303         }
   5304       }
   5305       return n;
   5306     };
   5307 
   5308     var queryLower = query.toLowerCase();
   5309     var queryAlnum = (queryLower.match(/\w+/) || [''])[0];
   5310     var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum);
   5311     var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b');
   5312 
   5313     var _resultScoreFn = function(result) {
   5314       // scores are calculated based on exact and prefix matches,
   5315       // and then number of path separators (dots) from the last
   5316       // match (i.e. favoring classes and deep package names)
   5317       var score = 1.0;
   5318       var labelLower = result.label.toLowerCase();
   5319       var t;
   5320       var partsAfter;
   5321       t = _lastSearch(labelLower, partExactAlnumRE);
   5322       if (t >= 0) {
   5323         // exact part match
   5324         partsAfter = _countChar(labelLower.substr(t + 1), '.');
   5325         score *= 200 / (partsAfter + 1);
   5326       } else {
   5327         t = _lastSearch(labelLower, partPrefixAlnumRE);
   5328         if (t >= 0) {
   5329           // part prefix match
   5330           partsAfter = _countChar(labelLower.substr(t + 1), '.');
   5331           score *= 20 / (partsAfter + 1);
   5332         }
   5333       }
   5334 
   5335       return score;
   5336     };
   5337 
   5338     for (var i = 0; i < matches.length; i++) {
   5339       // if the API is deprecated, default score is 0; otherwise, perform scoring
   5340       if (matches[i].deprecated === 'true') {
   5341         matches[i].__resultScore = 0;
   5342       } else {
   5343         matches[i].__resultScore = _resultScoreFn(matches[i]);
   5344       }
   5345     }
   5346 
   5347     matches.sort(function(a, b) {
   5348       var n = b.__resultScore - a.__resultScore;
   5349 
   5350       if (n === 0) {
   5351         // lexicographical sort if scores are the same
   5352         n = (a.label < b.label) ? -1 : 1;
   5353       }
   5354 
   5355       return n;
   5356     });
   5357   }
   5358 
   5359   // Destructive but fast toUnique.
   5360   // http://stackoverflow.com/a/25082874
   5361   function toUnique(array) {
   5362     var c;
   5363     var b = array.length || 1;
   5364 
   5365     while (c = --b) {
   5366       while (c--) {
   5367         if (array[b] === array[c]) {
   5368           array.splice(c, 1);
   5369         }
   5370       }
   5371     }
   5372     return array;
   5373   }
   5374 
   5375   return search;
   5376 })();
   5377 
   5378 (function($) {
   5379   'use strict';
   5380 
   5381   /**
   5382    * Smoothly scroll to location on current page.
   5383    * @param el
   5384    * @param options
   5385    * @constructor
   5386    */
   5387   function ScrollButton(el, options) {
   5388     this.el = $(el);
   5389     this.target = $(this.el.attr('href'));
   5390     this.options = $.extend({}, ScrollButton.DEFAULTS_, options);
   5391 
   5392     if (typeof this.options.offset === 'string') {
   5393       this.options.offset = $(this.options.offset).height();
   5394     }
   5395 
   5396     this.el.on('click', this.clickHandler_.bind(this));
   5397   }
   5398 
   5399   /**
   5400    * Default options
   5401    * @type {{duration: number, easing: string, offset: number, scrollContainer: string}}
   5402    * @private
   5403    */
   5404   ScrollButton.DEFAULTS_ = {
   5405     duration: 300,
   5406     easing: 'swing',
   5407     offset: '.dac-header',
   5408     scrollContainer: 'html, body'
   5409   };
   5410 
   5411   /**
   5412    * Scroll logic
   5413    * @param event
   5414    * @private
   5415    */
   5416   ScrollButton.prototype.clickHandler_ = function(event) {
   5417     if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
   5418       return;
   5419     }
   5420 
   5421     event.preventDefault();
   5422 
   5423     var position = this.getTargetPosition();
   5424     $(this.options.scrollContainer).animate({
   5425       scrollTop: position - this.options.offset
   5426     }, this.options);
   5427   };
   5428 
   5429   ScrollButton.prototype.getTargetPosition = function() {
   5430     if (this.options.scrollContainer === ScrollButton.DEFAULTS_.scrollContainer) {
   5431       return this.target.offset().top;
   5432     }
   5433     var scrollContainer = $(this.options.scrollContainer)[0];
   5434     var currentEl = this.target[0];
   5435     var pos = 0;
   5436     while (currentEl !== scrollContainer && currentEl !== null) {
   5437       pos += currentEl.offsetTop;
   5438       currentEl = currentEl.offsetParent;
   5439     }
   5440     return pos;
   5441   };
   5442 
   5443   /**
   5444    * jQuery plugin
   5445    * @param  {object} options - Override default options.
   5446    */
   5447   $.fn.dacScrollButton = function(options) {
   5448     return this.each(function() {
   5449       new ScrollButton(this, options);
   5450     });
   5451   };
   5452 
   5453   /**
   5454    * Data Attribute API
   5455    */
   5456   $(document).on('ready.aranja', function() {
   5457     $('[data-scroll-button]').each(function() {
   5458       $(this).dacScrollButton($(this).data());
   5459     });
   5460   });
   5461 })(jQuery);
   5462 
   5463 /* global getLangPref */
   5464 (function($) {
   5465   var LANG;
   5466 
   5467   function getSearchLang() {
   5468     if (!LANG) {
   5469       LANG = getLangPref();
   5470 
   5471       // Fix zh-cn to be zh-CN.
   5472       LANG = LANG.replace(/-\w+/, function(m) { return m.toUpperCase(); });
   5473     }
   5474     return LANG;
   5475   }
   5476 
   5477   function customSearch(query, start) {
   5478     var searchParams = {
   5479       // current cse instance:
   5480       //cx: '001482626316274216503:zu90b7s047u',
   5481       // new cse instance:
   5482       cx: '000521750095050289010:zpcpi1ea4s8',
   5483       key: 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8',
   5484       q: query,
   5485       start: start || 1,
   5486       num: 9,
   5487       hl: getSearchLang(),
   5488       fields: 'queries,items(pagemap,link,title,htmlSnippet,formattedUrl)'
   5489     };
   5490 
   5491     return $.get('https://content.googleapis.com/customsearch/v1?' +  $.param(searchParams));
   5492   }
   5493 
   5494   function renderResults(el, results, searchAppliance) {
   5495     var referenceResults = searchAppliance.getReferenceResults();
   5496     if (!results.items) {
   5497       el.append($('<div>').text('No results'));
   5498       return;
   5499     }
   5500 
   5501     for (var i = 0; i < results.items.length; i++) {
   5502       var item = results.items[i];
   5503       var isDuplicate = false;
   5504       $(referenceResults.android).each(function(index, result) {
   5505         if (item.link.indexOf(result.link) > -1) {
   5506           isDuplicate = true;
   5507           return false;
   5508         }
   5509       });
   5510 
   5511       if (!isDuplicate) {
   5512         var hasImage = item.pagemap && item.pagemap.cse_thumbnail;
   5513         var sectionMatch = item.link.match(/developer\.android\.com\/(\w*)/);
   5514         var section = (sectionMatch && sectionMatch[1]) || 'blog';
   5515 
   5516         var entry = $('<div>').addClass('dac-custom-search-entry cols');
   5517 
   5518         if (hasImage) {
   5519           var image = item.pagemap.cse_thumbnail[0];
   5520           entry.append($('<div>').addClass('dac-custom-search-image-wrapper')
   5521             .append($('<div>').addClass('dac-custom-search-image').css('background-image', 'url(' + image.src + ')')));
   5522         }
   5523 
   5524         entry.append($('<div>').addClass('dac-custom-search-text-wrapper')
   5525           .append($('<p>').addClass('dac-custom-search-section').text(section))
   5526           .append(
   5527             $('<a>').text(item.title).attr('href', item.link).wrap('<h2>').parent().addClass('dac-custom-search-title')
   5528           )
   5529           .append($('<p>').addClass('dac-custom-search-snippet').html(item.htmlSnippet.replace(/<br>/g, '')))
   5530           .append($('<a>').addClass('dac-custom-search-link').text(item.formattedUrl).attr('href', item.link)));
   5531 
   5532         el.append(entry);
   5533       }
   5534     }
   5535 
   5536     if (results.queries.nextPage) {
   5537       var loadMoreButton = $('<button id="dac-custom-search-load-more">')
   5538         .addClass('dac-custom-search-load-more')
   5539         .text('Load more')
   5540         .click(function() {
   5541           loadMoreResults(el, results, searchAppliance);
   5542         });
   5543 
   5544       el.append(loadMoreButton);
   5545     }
   5546   };
   5547 
   5548   function loadMoreResults(el, results, searchAppliance) {
   5549     var query = results.queries.request[0].searchTerms;
   5550     var start = results.queries.nextPage[0].startIndex;
   5551     var loadMoreButton = el.find('#dac-custom-search-load-more');
   5552 
   5553     loadMoreButton.text('Loading more...');
   5554 
   5555     customSearch(query, start).then(function(results) {
   5556       loadMoreButton.remove();
   5557       renderResults(el, results, searchAppliance);
   5558     });
   5559   }
   5560 
   5561   $.fn.customSearch = function(query, searchAppliance) {
   5562     var el = $(this);
   5563 
   5564     customSearch(query).then(function(results) {
   5565       el.empty();
   5566       renderResults(el, results, searchAppliance);
   5567     });
   5568   };
   5569 })(jQuery);
   5570 
   5571 /* global METADATA */
   5572 
   5573 (function($) {
   5574   $.fn.dacSearchRenderHero = function(resources, query) {
   5575     var el = $(this);
   5576     el.empty();
   5577 
   5578     var resource = METADATA.searchHeroCollections[query];
   5579 
   5580     if (resource) {
   5581       el.dacHero(resource, true);
   5582       el.show();
   5583 
   5584       return true;
   5585     } else {
   5586       el.hide();
   5587     }
   5588   };
   5589 })(jQuery);
   5590 
   5591 (function($) {
   5592   $.fn.dacSearchRenderReferences = function(results, query) {
   5593     var referenceCard = $('.suggest-card.reference');
   5594     referenceCard.data('searchreferences.dac', {results: results, query: query});
   5595     renderResults(referenceCard, results, query, false);
   5596   };
   5597 
   5598   var ROW_COUNT_COLLAPSED = 20;
   5599   var ROW_COUNT_EXPANDED = 40;
   5600   var ROW_COUNT_GOOGLE_COLLAPSED = 1;
   5601   var ROW_COUNT_GOOGLE_EXPANDED = 8;
   5602 
   5603   function onSuggestionClick(e) {
   5604     devsite.analytics.trackAnalyticsEvent('event',
   5605         'Suggestion Click', 'clicked: ' + $(e.currentTarget).attr('href'),
   5606         'query: ' + $('#search_autocomplete').val().toLowerCase());
   5607   }
   5608 
   5609   function buildLink(match) {
   5610     var link = $('<a>').attr('href', window.toRoot + match.link);
   5611 
   5612     var label = match.label;
   5613     var classNameStart = label.match(/[A-Z]/) ? label.search(/[A-Z]/) : label.lastIndexOf('.') + 1;
   5614     var newLink = '<span class="namespace">' +
   5615       label.substr(0, classNameStart) +
   5616       '</span>' +
   5617       label.substr(classNameStart, label.length);
   5618 
   5619     link.html(newLink);
   5620     return link;
   5621   }
   5622 
   5623   function buildSuggestion(match, query) {
   5624     var li = $('<li>').addClass('dac-search-results-reference-entry');
   5625 
   5626     var link = buildLink(match);
   5627     link.highlightMatches(query);
   5628     li.append(link);
   5629     return li[0];
   5630   }
   5631 
   5632   function buildResults(results, query) {
   5633     return results.map(function(match) {
   5634       return buildSuggestion(match, query);
   5635     });
   5636   }
   5637 
   5638   function renderAndroidResults(list, gMatches, query) {
   5639     list.empty();
   5640 
   5641     var header = $('<li class="dac-search-results-reference-header">android APIs</li>');
   5642     list.append(header);
   5643 
   5644     if (gMatches.length > 0) {
   5645       list.removeClass('no-results');
   5646 
   5647       var resources = buildResults(gMatches, query);
   5648       list.append(resources);
   5649       return true;
   5650     } else {
   5651       list.append('<li class="dac-search-results-reference-entry-empty">No results</li>');
   5652     }
   5653   }
   5654 
   5655   function renderGoogleDocsResults(list, gGoogleMatches, query) {
   5656     list = $('.suggest-card.reference ul');
   5657 
   5658     if (gGoogleMatches.length > 0) {
   5659       list.append('<li class="dac-search-results-reference-header">in Google Services</li>');
   5660 
   5661       var resources = buildResults(gGoogleMatches, query);
   5662       list.append(resources);
   5663 
   5664       return true;
   5665     }
   5666   }
   5667 
   5668   function renderResults(referenceCard, results, query, expanded) {
   5669     var list = referenceCard.find('ul');
   5670     list.toggleClass('is-expanded', !!expanded);
   5671 
   5672     // Figure out how many results we can show in our fixed size box.
   5673     var total = expanded ? ROW_COUNT_EXPANDED : ROW_COUNT_COLLAPSED;
   5674     var googleCount = expanded ? ROW_COUNT_GOOGLE_EXPANDED : ROW_COUNT_GOOGLE_COLLAPSED;
   5675     googleCount = Math.max(googleCount, total - results.android.length);
   5676     googleCount = Math.min(googleCount, results.docs.length);
   5677 
   5678     if (googleCount > 0) {
   5679       // If there are google results, reserve space for its header.
   5680       googleCount++;
   5681     }
   5682 
   5683     var androidCount = Math.max(0, total - googleCount);
   5684     if (androidCount === 0) {
   5685       // Reserve space for "No reference results"
   5686       googleCount--;
   5687     }
   5688 
   5689     renderAndroidResults(list, results.android.slice(0, androidCount), query);
   5690     renderGoogleDocsResults(list, results.docs.slice(0, googleCount - 1), query);
   5691 
   5692     var totalResults = results.android.length + results.docs.length;
   5693     if (totalResults === 0) {
   5694       list.addClass('no-results');
   5695     }
   5696 
   5697     // Tweak see more logic to account for references.
   5698     var hasMore = totalResults > ROW_COUNT_COLLAPSED && !util.matchesMedia('mobile');
   5699     if (hasMore) {
   5700       // We can't actually show all matches, only as many as the expanded list
   5701       // will fit, so we actually lie if the total results count is more
   5702       var moreCount = Math.min(totalResults, ROW_COUNT_EXPANDED + ROW_COUNT_GOOGLE_EXPANDED);
   5703       var $moreLink = $('<li class="dac-search-results-reference-entry-empty " data-toggle="show-more">see more matches</li>');
   5704       list.append($moreLink.on('click', onToggleMore));
   5705     }
   5706     var searchEl = $('#search-resources');
   5707     searchEl.toggleClass('dac-has-more', searchEl.hasClass('dac-has-more') || (hasMore && !expanded));
   5708     searchEl.toggleClass('dac-has-less', searchEl.hasClass('dac-has-less') || (hasMore && expanded));
   5709   }
   5710 
   5711   function onToggleMore(e) {
   5712     var link = $(e.currentTarget);
   5713     var referenceCard = $('.suggest-card.reference');
   5714     var data = referenceCard.data('searchreferences.dac');
   5715 
   5716     if (util.matchesMedia('mobile')) { return; }
   5717 
   5718     renderResults(referenceCard, data.results, data.query, link.data('toggle') === 'show-more');
   5719   }
   5720 
   5721   $(document).on('click', '.dac-search-results-resources [data-toggle="show-more"]', onToggleMore);
   5722   $(document).on('click', '.dac-search-results-resources [data-toggle="show-less"]', onToggleMore);
   5723   $(document).on('click', '.suggest-card.reference a', onSuggestionClick);
   5724 })(jQuery);
   5725 
   5726 (function($) {
   5727   function highlightPage(query, page) {
   5728     page.find('.title').highlightMatches(query);
   5729   }
   5730 
   5731   $.fn.dacSearchRenderResources = function(gDocsMatches, query) {
   5732     this.resourceWidget(gDocsMatches, {
   5733       itemsPerPage: 18,
   5734       initialResults: 6,
   5735       cardSizes: ['6x2'],
   5736       onRenderPage: highlightPage.bind(null, query)
   5737     });
   5738 
   5739     return this;
   5740   };
   5741 })(jQuery);
   5742 
   5743 /*global metadata */
   5744 
   5745 (function($, metadata) {
   5746   'use strict';
   5747 
   5748   function Search() {
   5749     this.body = $('body');
   5750     this.lastQuery = null;
   5751     this.searchResults = $('#search-results');
   5752     this.searchClose = $('[data-search-close]');
   5753     this.searchClear = $('[data-search-clear]');
   5754     this.searchInput = $('#search_autocomplete');
   5755     this.searchResultsContent = $('#dac-search-results-content');
   5756     this.searchResultsFor = $('#search-results-for');
   5757     this.searchResultsHistory = $('#dac-search-results-history');
   5758     this.searchResultsResources = $('#search-resources');
   5759     this.searchResultsHero = $('#dac-search-results-hero');
   5760     this.searchResultsReference = $('#dac-search-results-reference');
   5761     this.searchHeader = $('[data-search]').data('search-input.dac');
   5762     this.pageNav = $('a[name=navigation]');
   5763     this.currQueryReferenceResults = {};
   5764     this.isOpen = false;
   5765   }
   5766 
   5767   Search.prototype.init = function() {
   5768     this.searchHistory = window.dacStore('search-history');
   5769 
   5770     this.searchInput.focus(this.onSearchChanged.bind(this));
   5771     this.searchInput.keypress(this.handleKeyboardShortcut.bind(this));
   5772     this.pageNav.keyup(this.handleTabbedToNav.bind(this));
   5773     this.searchResults.keyup(this.handleKeyboardShortcut.bind(this));
   5774     this.searchInput.on('input', this.onSearchChanged.bind(this));
   5775     this.searchClear.click(this.clear.bind(this));
   5776     this.searchClose.click(this.close.bind(this));
   5777 
   5778     this.customSearch = $.fn.debounce(function(query) {
   5779       $('#dac-custom-search-results').customSearch(query, this);
   5780     }.bind(this), 1000);
   5781     // Start search shortcut (/)
   5782     $('body').keyup(function(event) {
   5783       if (event.which === 191 && $(event.target).is(':not(:input)')) {
   5784         this.searchInput.focus();
   5785       }
   5786     }.bind(this));
   5787 
   5788     $(window).on('popstate', this.onPopState.bind(this));
   5789     $(window).hashchange(this.onHashChange.bind(this));
   5790     this.onHashChange();
   5791   };
   5792 
   5793   Search.prototype.checkRedirectToIndex = function() {
   5794     var query = this.getUrlQuery();
   5795     var target = window.getLangTarget();
   5796     var prefix = (target !== 'en') ? '/intl/' + target : '';
   5797     var pathname = location.pathname.slice(prefix.length);
   5798     if (query != null && pathname !== '/index.html') {
   5799       location.href = prefix + '/index.html' + location.hash;
   5800       return true;
   5801     }
   5802   };
   5803 
   5804   Search.prototype.handleKeyboardShortcut = function(event) {
   5805     // Close (esc)
   5806     if (event.which === 27) {
   5807       this.searchClose.trigger('click');
   5808       event.preventDefault();
   5809     }
   5810 
   5811     // Previous result (up arrow)
   5812     if (event.which === 38) {
   5813       this.previousResult();
   5814       event.preventDefault();
   5815     }
   5816 
   5817     // Next result (down arrow)
   5818     if (event.which === 40) {
   5819       this.nextResult();
   5820       event.preventDefault();
   5821     }
   5822 
   5823     // Navigate to result (enter)
   5824     if (event.which === 13) {
   5825       this.navigateToResult();
   5826       event.preventDefault();
   5827     }
   5828   };
   5829 
   5830   Search.prototype.handleTabbedToNav = function(event) {
   5831     if (this.isOpen) {
   5832       this.searchClose.trigger('click');
   5833     }
   5834   }
   5835 
   5836   Search.prototype.goToResult = function(relativeIndex) {
   5837     var links = this.searchResults.find('a').filter(':visible');
   5838     var selectedLink = this.searchResults.find('.dac-selected');
   5839 
   5840     if (selectedLink.length) {
   5841       var found = $.inArray(selectedLink[0], links);
   5842 
   5843       selectedLink.removeClass('dac-selected');
   5844       links.eq(found + relativeIndex).addClass('dac-selected');
   5845       return true;
   5846     } else {
   5847       if (relativeIndex > 0) {
   5848         links.first().addClass('dac-selected');
   5849       }
   5850     }
   5851   };
   5852 
   5853   Search.prototype.previousResult = function() {
   5854     this.goToResult(-1);
   5855   };
   5856 
   5857   Search.prototype.nextResult = function() {
   5858     this.goToResult(1);
   5859   };
   5860 
   5861   Search.prototype.navigateToResult = function() {
   5862     var query = this.getQuery();
   5863     var selectedLink = this.searchResults.find('.dac-selected');
   5864 
   5865     if (selectedLink.length) {
   5866       selectedLink[0].click();
   5867     } else {
   5868       this.searchHistory.push(query);
   5869       this.addQueryToUrl(query);
   5870 
   5871       var isMobileOrTablet = typeof window.orientation !== 'undefined';
   5872 
   5873       if (isMobileOrTablet) {
   5874         this.searchInput.blur();
   5875       }
   5876     }
   5877   };
   5878 
   5879   Search.prototype.onHashChange = function() {
   5880     var query = this.getUrlQuery();
   5881     if (query != null && query !== this.getQuery()) {
   5882       this.searchInput.val(query);
   5883       this.onSearchChanged();
   5884     }
   5885   };
   5886 
   5887   Search.prototype.clear = function() {
   5888     this.searchInput.val('');
   5889     window.location.hash = '';
   5890     this.onSearchChanged();
   5891     this.searchInput.focus();
   5892   };
   5893 
   5894   Search.prototype.close = function() {
   5895     this.removeQueryFromUrl();
   5896     this.searchInput.blur();
   5897     this.hideOverlay();
   5898     this.pageNav.focus();
   5899     this.isOpen = false;
   5900   };
   5901 
   5902   Search.prototype.getUrlQuery = function() {
   5903     var queryMatch = location.hash.match(/q=(.*)&?/);
   5904     return queryMatch && queryMatch[1] && decodeURI(queryMatch[1]);
   5905   };
   5906 
   5907   Search.prototype.getQuery = function() {
   5908     return this.searchInput.val().replace(/(^ +)|( +$)/g, '');
   5909   };
   5910 
   5911   Search.prototype.getReferenceResults = function() {
   5912     return this.currQueryReferenceResults;
   5913   };
   5914 
   5915   Search.prototype.onSearchChanged = function() {
   5916     var query = this.getQuery();
   5917 
   5918     this.showOverlay();
   5919     this.render(query);
   5920   };
   5921 
   5922   Search.prototype.render = function(query) {
   5923     if (this.lastQuery === query) { return; }
   5924 
   5925     if (query.length < 2) {
   5926       query = '';
   5927     }
   5928 
   5929     this.lastQuery = query;
   5930     this.searchResultsFor.text(query);
   5931 
   5932     // CSE results lag behind the metadata/reference results. We need to empty
   5933     // the CSE results and add 'Loading' text so user's aren't looking at two
   5934     // different sets of search results at one time.
   5935     var $loadingEl =
   5936         $('<div class="loadingCustomSearchResults">Loading Results...</div>');
   5937     $('#dac-custom-search-results').empty().prepend($loadingEl);
   5938 
   5939     this.customSearch(query);
   5940     var metadataResults = metadata.search(query);
   5941     this.searchResultsResources.dacSearchRenderResources(metadataResults.resources, query);
   5942     this.searchResultsReference.dacSearchRenderReferences(metadataResults, query);
   5943     this.currQueryReferenceResults = metadataResults;
   5944     var hasHero = this.searchResultsHero.dacSearchRenderHero(metadataResults.resources, query);
   5945     var hasQuery = !!query;
   5946 
   5947     this.searchResultsReference.toggle(!hasHero);
   5948     this.searchResultsContent.toggle(hasQuery);
   5949     this.searchResultsHistory.toggle(!hasQuery);
   5950     this.addQueryToUrl(query);
   5951     this.pushState();
   5952   };
   5953 
   5954   Search.prototype.addQueryToUrl = function(query) {
   5955     var hash = 'q=' + encodeURI(query);
   5956 
   5957     if (query) {
   5958       if (window.history.replaceState) {
   5959         window.history.replaceState(null, '', '#' + hash);
   5960       } else {
   5961         window.location.hash = hash;
   5962       }
   5963     }
   5964   };
   5965 
   5966   Search.prototype.onPopState = function() {
   5967     if (!this.getUrlQuery()) {
   5968       this.hideOverlay();
   5969       this.searchHeader.unsetActiveState();
   5970     }
   5971   };
   5972 
   5973   Search.prototype.removeQueryFromUrl = function() {
   5974     window.location.hash = '';
   5975   };
   5976 
   5977   Search.prototype.pushState = function() {
   5978     if (window.history.pushState && !this.lastQuery.length) {
   5979       window.history.pushState(null, '');
   5980     }
   5981   };
   5982 
   5983   Search.prototype.showOverlay = function() {
   5984     this.isOpen = true;
   5985     this.body.addClass('dac-modal-open dac-search-open');
   5986   };
   5987 
   5988   Search.prototype.hideOverlay = function() {
   5989     this.body.removeClass('dac-modal-open dac-search-open');
   5990   };
   5991 
   5992   $(document).on('ready.aranja', function() {
   5993     var search = new Search();
   5994     search.init();
   5995   });
   5996 })(jQuery, metadata);
   5997 
   5998 window.dacStore = (function(window) {
   5999   /**
   6000    * Creates a new persistent store.
   6001    * If localStorage is unavailable, the items are stored in memory.
   6002    *
   6003    * @constructor
   6004    * @param {string} name    The name of the store
   6005    * @param {number} maxSize The maximum number of items the store can hold.
   6006    */
   6007   var Store = function(name, maxSize) {
   6008     var content = [];
   6009 
   6010     var hasLocalStorage = !!window.localStorage;
   6011 
   6012     if (hasLocalStorage) {
   6013       try {
   6014         content = JSON.parse(window.localStorage.getItem(name) || []);
   6015       } catch (e) {
   6016         // Store contains invalid data
   6017         window.localStorage.removeItem(name);
   6018       }
   6019     }
   6020 
   6021     function push(item) {
   6022       if (content[0] === item) {
   6023         return;
   6024       }
   6025 
   6026       content.unshift(item);
   6027 
   6028       if (maxSize) {
   6029         content.splice(maxSize, content.length);
   6030       }
   6031 
   6032       if (hasLocalStorage) {
   6033         window.localStorage.setItem(name, JSON.stringify(content));
   6034       }
   6035     }
   6036 
   6037     function all() {
   6038       // Return a copy
   6039       return content.slice();
   6040     }
   6041 
   6042     return {
   6043       push: push,
   6044       all: all
   6045     };
   6046   };
   6047 
   6048   var stores = {
   6049     'search-history': new Store('search-history', 3)
   6050   };
   6051 
   6052   /**
   6053    * Get a named persistent store.
   6054    * @param  {string} name
   6055    * @return {Store}
   6056    */
   6057   return function getStore(name) {
   6058     return stores[name];
   6059   };
   6060 })(window);
   6061 
   6062 (function($) {
   6063   'use strict';
   6064 
   6065   /**
   6066    * A component that swaps two dynamic height views with an animation.
   6067    * Listens for the following events:
   6068    * * swap-content: triggers SwapContent.swap_()
   6069    * * swap-reset: triggers SwapContent.reset()
   6070    * @param el
   6071    * @param options
   6072    * @constructor
   6073    */
   6074   function SwapContent(el, options) {
   6075     this.el = $(el);
   6076     this.options = $.extend({}, SwapContent.DEFAULTS_, options);
   6077     this.options.dynamic = this.options.dynamic === 'true';
   6078     this.containers = this.el.find(this.options.container);
   6079     this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0);
   6080     this.el.on('swap-content', this.swap.bind(this));
   6081     this.el.on('swap-reset', this.reset.bind(this));
   6082     this.el.find(this.options.swapButton).on('click keypress', function(e) {
   6083       if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
   6084         this.swap();
   6085       }
   6086     }.bind(this));
   6087   }
   6088 
   6089   /**
   6090    * SwapContent's default settings.
   6091    * @type {{activeClass: string, container: string, transitionSpeed: number}}
   6092    * @private
   6093    */
   6094   SwapContent.DEFAULTS_ = {
   6095     activeClass: 'dac-active',
   6096     container: '[data-swap-container]',
   6097     dynamic: 'true',
   6098     swapButton: '[data-swap-button]',
   6099     transitionSpeed: 500
   6100   };
   6101 
   6102   /**
   6103    * Returns container's visible height.
   6104    * @param container
   6105    * @returns {number}
   6106    */
   6107   SwapContent.prototype.currentHeight = function(container) {
   6108     return container.children('.' + this.options.activeClass).outerHeight();
   6109   };
   6110 
   6111   /**
   6112    * Reset to show initial content
   6113    */
   6114   SwapContent.prototype.reset = function() {
   6115     if (!this.initiallyActive.hasClass(this.initiallyActive)) {
   6116       this.containers.children().toggleClass(this.options.activeClass);
   6117     }
   6118   };
   6119 
   6120   /**
   6121    * Complete the swap.
   6122    */
   6123   SwapContent.prototype.complete = function() {
   6124     this.containers.height('auto');
   6125     this.containers.trigger('swap-complete');
   6126   };
   6127 
   6128   /**
   6129    * Perform the swap of content.
   6130    */
   6131   SwapContent.prototype.swap = function() {
   6132     this.containers.each(function(index, container) {
   6133       container = $(container);
   6134 
   6135       if (!this.options.dynamic) {
   6136         container.children().toggleClass(this.options.activeClass);
   6137         this.complete.bind(this);
   6138         $('.' + this.options.activeClass).focus();
   6139         return;
   6140       }
   6141 
   6142       container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass);
   6143       container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed,
   6144         this.complete.bind(this));
   6145     }.bind(this));
   6146   };
   6147 
   6148   /**
   6149    * jQuery plugin
   6150    * @param  {object} options - Override default options.
   6151    */
   6152   $.fn.dacSwapContent = function(options) {
   6153     return this.each(function() {
   6154       new SwapContent(this, options);
   6155     });
   6156   };
   6157 
   6158   /**
   6159    * Data Attribute API
   6160    */
   6161   $(document).on('ready.aranja', function() {
   6162     $('[data-swap]').each(function() {
   6163       $(this).dacSwapContent($(this).data());
   6164     });
   6165   });
   6166 })(jQuery);
   6167 
   6168 /* Tabs */
   6169 (function($) {
   6170   'use strict';
   6171 
   6172   /**
   6173    * @param {HTMLElement} el - The DOM element.
   6174    * @param {Object} options
   6175    * @constructor
   6176    */
   6177   function Tabs(el, options) {
   6178     this.el = $(el);
   6179     this.options = $.extend({}, Tabs.DEFAULTS_, options);
   6180     this.init();
   6181   }
   6182 
   6183   Tabs.DEFAULTS_ = {
   6184     activeClass: 'dac-active',
   6185     viewDataAttr: 'tab-view',
   6186     itemDataAttr: 'tab-item'
   6187   };
   6188 
   6189   Tabs.prototype.init = function() {
   6190     var itemDataAttribute = '[data-' + this.options.itemDataAttr + ']';
   6191     this.tabEl_ = this.el.find(itemDataAttribute);
   6192     this.tabViewEl_ = this.el.find('[data-' + this.options.viewDataAttr + ']');
   6193     this.el.on('click.dac-tabs', itemDataAttribute, this.changeTabs.bind(this));
   6194   };
   6195 
   6196   Tabs.prototype.changeTabs = function(event) {
   6197     var current = $(event.currentTarget);
   6198     var index = current.index();
   6199 
   6200     if (current.hasClass(this.options.activeClass)) {
   6201       current.add(this.tabViewEl_.eq(index)).removeClass(this.options.activeClass);
   6202     } else {
   6203       this.tabEl_.add(this.tabViewEl_).removeClass(this.options.activeClass);
   6204       current.add(this.tabViewEl_.eq(index)).addClass(this.options.activeClass);
   6205     }
   6206   };
   6207 
   6208   /**
   6209    * jQuery plugin
   6210    */
   6211   $.fn.dacTabs = function() {
   6212     return this.each(function() {
   6213       var el = $(this);
   6214       new Tabs(el, el.data());
   6215     });
   6216   };
   6217 
   6218   /**
   6219    * Data Attribute API
   6220    */
   6221   $(function() {
   6222     $('[data-tabs]').dacTabs();
   6223   });
   6224 })(jQuery);
   6225 
   6226 /* Toast Component */
   6227 (function($) {
   6228   'use strict';
   6229   /**
   6230    * @constant
   6231    * @type {String}
   6232    */
   6233   var LOCAL_STORAGE_KEY = 'toast-closed-index';
   6234 
   6235   /**
   6236    * Dictionary from local storage.
   6237    */
   6238   var toastDictionary = localStorage.getItem(LOCAL_STORAGE_KEY);
   6239   toastDictionary = toastDictionary ? JSON.parse(toastDictionary) : {};
   6240 
   6241   /**
   6242    * Variable used for caching the body.
   6243    */
   6244   var bodyCached;
   6245 
   6246   /**
   6247    * @param {HTMLElement} el - The DOM element.
   6248    * @param {Object} options
   6249    * @constructor
   6250    */
   6251   function Toast(el, options) {
   6252     this.el = $(el);
   6253     this.options = $.extend({}, Toast.DEFAULTS_, options);
   6254     this.init();
   6255   }
   6256 
   6257   Toast.DEFAULTS_ = {
   6258     closeBtnClass: 'dac-toast-close-btn',
   6259     closeDuration: 200,
   6260     visibleClass: 'dac-visible',
   6261     wrapClass: 'dac-toast-wrap'
   6262   };
   6263 
   6264   /**
   6265    * Generate a close button.
   6266    * @returns {*|HTMLElement}
   6267    */
   6268   Toast.prototype.closeBtn = function() {
   6269     this.closeBtnEl = this.closeBtnEl || $('<button class="' + this.options.closeBtnClass + '">' +
   6270       '<span class="dac-button dac-raised dac-primary">OK</span>' +
   6271     '</button>');
   6272     return this.closeBtnEl;
   6273   };
   6274 
   6275   /**
   6276    * Initialize a new toast element
   6277    */
   6278   Toast.prototype.init = function() {
   6279     this.hash = this.el.text().replace(/[\s\n\t]/g, '').split('').slice(0, 128).join('');
   6280 
   6281     if (toastDictionary[this.hash]) {
   6282       return;
   6283     }
   6284 
   6285     this.closeBtn().on('click', this.onClickHandler.bind(this));
   6286     this.el.find('.' + this.options.wrapClass).append(this.closeBtn());
   6287     this.el.addClass(this.options.visibleClass);
   6288     this.dynamicPadding(this.el.outerHeight());
   6289   };
   6290 
   6291   /**
   6292    * Add padding to make sure all page is visible.
   6293    */
   6294   Toast.prototype.dynamicPadding = function(val) {
   6295     var currentPadding = parseInt(bodyCached.css('padding-bottom') || 0);
   6296     bodyCached.css('padding-bottom', val + currentPadding);
   6297   };
   6298 
   6299   /**
   6300    * Remove a toast from the DOM
   6301    */
   6302   Toast.prototype.remove = function() {
   6303     this.dynamicPadding(-this.el.outerHeight());
   6304     this.el.remove();
   6305   };
   6306 
   6307   /**
   6308    * Handle removal of the toast.
   6309    */
   6310   Toast.prototype.onClickHandler = function() {
   6311     // Only fadeout toasts from top of stack. Others are removed immediately.
   6312     var duration = this.el.index() === 0 ? this.options.closeDuration : 0;
   6313     this.el.fadeOut(duration, this.remove.bind(this));
   6314 
   6315     // Save closed state.
   6316     toastDictionary[this.hash] = 1;
   6317     localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(toastDictionary));
   6318   };
   6319 
   6320   /**
   6321    * jQuery plugin
   6322    * @param  {object} options - Override default options.
   6323    */
   6324   $.fn.dacToast = function() {
   6325     return this.each(function() {
   6326       var el = $(this);
   6327       new Toast(el, el.data());
   6328     });
   6329   };
   6330 
   6331   /**
   6332    * Data Attribute API
   6333    */
   6334   $(function() {
   6335     bodyCached = $('#body-content');
   6336     $('[data-toast]').dacToast();
   6337   });
   6338 })(jQuery);
   6339 
   6340 (function($) {
   6341   function Toggle(el) {
   6342     $(el).on('click.dac.togglesection', this.toggle);
   6343   }
   6344 
   6345   Toggle.prototype.toggle = function() {
   6346     var $this = $(this);
   6347 
   6348     var $parent = getParent($this);
   6349     var isExpanded = $parent.hasClass('is-expanded');
   6350 
   6351     transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded);
   6352     $parent.toggleClass('is-expanded');
   6353 
   6354     return false;
   6355   };
   6356 
   6357   function getParent($this) {
   6358     var selector = $this.attr('data-target');
   6359 
   6360     if (!selector) {
   6361       selector = $this.attr('href');
   6362       selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
   6363     }
   6364 
   6365     var $parent = selector && $(selector);
   6366 
   6367     $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle');
   6368 
   6369     return $parent.length ? $parent : $this.parent();
   6370   }
   6371 
   6372   /**
   6373    * Runs a transition of max-height along with responsive styles which hide or expand the element.
   6374    * @param $el
   6375    * @param visible
   6376    */
   6377   function transitionMaxHeight($el, visible) {
   6378     var contentHeight = $el.prop('scrollHeight');
   6379     var targetHeight = visible ? contentHeight : 0;
   6380     var duration = $el.transitionDuration();
   6381 
   6382     // If we're hiding, first set the maxHeight we're transitioning from.
   6383     if (!visible) {
   6384       $el.css({
   6385           transitionDuration: '0s',
   6386           maxHeight: contentHeight + 'px'
   6387         })
   6388         .resolveStyles()
   6389         .css('transitionDuration', '');
   6390     }
   6391 
   6392     // Transition to new state
   6393     $el.css('maxHeight', targetHeight);
   6394 
   6395     // Reset maxHeight to css value after transition.
   6396     setTimeout(function() {
   6397       $el.css({
   6398           transitionDuration: '0s',
   6399           maxHeight: ''
   6400         })
   6401         .resolveStyles()
   6402         .css('transitionDuration', '');
   6403     }, duration);
   6404   }
   6405 
   6406   // Utility to get the transition duration for the element.
   6407   $.fn.transitionDuration = function() {
   6408     var d = $(this).css('transitionDuration') || '0s';
   6409 
   6410     return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0);
   6411   };
   6412 
   6413   // jQuery plugin
   6414   $.fn.toggleSection = function(option) {
   6415     return this.each(function() {
   6416       var $this = $(this);
   6417       var data = $this.data('dac.togglesection');
   6418       if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));}
   6419       if (typeof option === 'string') {data[option].call($this);}
   6420     });
   6421   };
   6422 
   6423   // Data api
   6424   $(document)
   6425     .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle);
   6426 })(jQuery);
   6427 
   6428 (function(window) {
   6429   /**
   6430    * Media query breakpoints. Should match CSS.
   6431    */
   6432   var BREAKPOINTS = {
   6433     mobile: [0, 719],
   6434     tablet: [720, 959],
   6435     desktop: [960, 9999]
   6436   };
   6437 
   6438   /**
   6439    * Fisher-Yates Shuffle (Knuth shuffle).
   6440    * @param {Array} input
   6441    * @returns {Array} shuffled array.
   6442    */
   6443   function shuffle(input) {
   6444     for (var i = input.length; i >= 0; i--) {
   6445       var randomIndex = Math.floor(Math.random() * (i + 1));
   6446       var randomItem = input[randomIndex];
   6447       input[randomIndex] = input[i];
   6448       input[i] = randomItem;
   6449     }
   6450 
   6451     return input;
   6452   }
   6453 
   6454   /**
   6455    * Matches media breakpoints like in CSS.
   6456    * @param {string} form of either mobile, tablet or desktop.
   6457    */
   6458   function matchesMedia(form) {
   6459     var breakpoint = BREAKPOINTS[form];
   6460     return window.innerWidth >= breakpoint[0] && window.innerWidth <= breakpoint[1];
   6461   }
   6462 
   6463   window.util = {
   6464     shuffle: shuffle,
   6465     matchesMedia: matchesMedia
   6466   };
   6467 })(window);
   6468 
   6469 (function($, window) {
   6470   'use strict';
   6471 
   6472   var YouTubePlayer = (function() {
   6473     var player;
   6474 
   6475     function VideoPlayer() {
   6476       this.mPlayerPaused = false;
   6477       this.doneSetup = false;
   6478     }
   6479 
   6480     VideoPlayer.prototype.setup = function() {
   6481       // loads the IFrame Player API code asynchronously.
   6482       $.getScript('https://www.youtube.com/iframe_api');
   6483 
   6484       // Add the shadowbox HTML to the body
   6485       $('body').prepend(
   6486 '<div id="video-player" class="Video">' +
   6487   '<div id="video-overlay" class="Video-overlay" />' +
   6488   '<div class="Video-container">' +
   6489     '<div class="Video-frame">' +
   6490       '<span class="Video-loading">Loading&hellip;</span>' +
   6491       '<div id="youTubePlayer"></div>' +
   6492     '</div>' +
   6493     '<div class="Video-controls">' +
   6494       '<button id="picture-in-picture" class="Video-button Video-button--picture-in-picture">' +
   6495       '<button id="close-video" class="Video-button Video-button--close" />' +
   6496     '</div>' +
   6497   '</div>' +
   6498 '</div>');
   6499 
   6500       this.videoPlayer = $('#video-player');
   6501 
   6502       var pictureInPictureButton = this.videoPlayer.find('#picture-in-picture');
   6503       pictureInPictureButton.on('click.aranja', this.toggleMinimizeVideo.bind(this));
   6504 
   6505       var videoOverlay = this.videoPlayer.find('#video-overlay');
   6506       var closeButton = this.videoPlayer.find('#close-video');
   6507       var closeVideo = this.closeVideo.bind(this);
   6508       videoOverlay.on('click.aranja', closeVideo);
   6509       closeButton.on('click.aranja', closeVideo);
   6510 
   6511       this.doneSetup = true;
   6512     };
   6513 
   6514     VideoPlayer.prototype.startYouTubePlayer = function(videoId) {
   6515       this.videoPlayer.show();
   6516 
   6517       if (!this.isLoaded) {
   6518         this.queueVideo = videoId;
   6519         return;
   6520       }
   6521 
   6522       this.mPlayerPaused = false;
   6523       // check if we've already created this player
   6524       if (!this.youTubePlayer) {
   6525         // check if there's a start time specified
   6526         var idAndHash = videoId.split('#');
   6527         var startTime = 0;
   6528         if (idAndHash.length > 1) {
   6529           startTime = idAndHash[1].split('t=')[1] !== undefined ? idAndHash[1].split('t=')[1] : 0;
   6530         }
   6531         // enable localized player
   6532         var lang = getLangPref();
   6533         var captionsOn = lang === 'en' ? 0 : 1;
   6534 
   6535         this.youTubePlayer = new YT.Player('youTubePlayer', {
   6536           height: 720,
   6537           width: 1280,
   6538           videoId: idAndHash[0],
   6539           // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
   6540           playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn},
   6541           // jscs:enable
   6542           events: {
   6543             'onReady': this.onPlayerReady.bind(this),
   6544             'onStateChange': this.onPlayerStateChange.bind(this)
   6545           }
   6546         });
   6547       } else {
   6548         // if a video different from the one already playing was requested, cue it up
   6549         if (videoId !== this.getVideoId()) {
   6550           this.youTubePlayer.cueVideoById(videoId);
   6551         }
   6552         this.youTubePlayer.playVideo();
   6553       }
   6554     };
   6555 
   6556     VideoPlayer.prototype.onPlayerReady = function(event) {
   6557       if (!isMobile) {
   6558         event.target.playVideo();
   6559         this.mPlayerPaused = false;
   6560       }
   6561     };
   6562 
   6563     VideoPlayer.prototype.toggleMinimizeVideo = function(event) {
   6564       event.stopPropagation();
   6565       this.videoPlayer.toggleClass('Video--picture-in-picture');
   6566     };
   6567 
   6568     VideoPlayer.prototype.closeVideo = function() {
   6569       try {
   6570         this.youTubePlayer.pauseVideo();
   6571       } catch (e) {
   6572       }
   6573       this.videoPlayer.fadeOut(200, function() {
   6574         this.videoPlayer.removeClass('Video--picture-in-picture');
   6575       }.bind(this));
   6576     };
   6577 
   6578     VideoPlayer.prototype.getVideoId = function() {
   6579       // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
   6580       return this.youTubePlayer && this.youTubePlayer.getVideoData().video_id;
   6581       // jscs:enable
   6582     };
   6583 
   6584     /* Track youtube playback for analytics */
   6585     VideoPlayer.prototype.onPlayerStateChange = function(event) {
   6586       var videoId = this.getVideoId();
   6587       var currentTime = this.youTubePlayer && this.youTubePlayer.getCurrentTime();
   6588 
   6589       // Video starts, send the video ID
   6590       if (event.data === YT.PlayerState.PLAYING) {
   6591         if (this.mPlayerPaused) {
   6592           devsite.analytics.trackAnalyticsEvent('event',
   6593               'Videos', 'Resume', videoId);
   6594         } else {
   6595           // track the start playing event so we know from which page the video was selected
   6596           devsite.analytics.trackAnalyticsEvent('event',
   6597               'Videos', 'Start: ' + videoId, 'on: ' + document.location.href);
   6598         }
   6599         this.mPlayerPaused = false;
   6600       }
   6601 
   6602       // Video paused, send video ID and video elapsed time
   6603       if (event.data === YT.PlayerState.PAUSED) {
   6604         devsite.analytics.trackAnalyticsEvent('event',
   6605             'Videos', 'Paused: ' + videoId, 'on: ' + currentTime);
   6606         this.mPlayerPaused = true;
   6607       }
   6608 
   6609       // Video finished, send video ID and video elapsed time
   6610       if (event.data === YT.PlayerState.ENDED) {
   6611         devsite.analytics.trackAnalyticsEvent('event',
   6612             'Videos', 'Finished: ' + videoId, 'on: ' + currentTime);
   6613         this.mPlayerPaused = true;
   6614       }
   6615     };
   6616 
   6617     return {
   6618       getPlayer: function() {
   6619         if (!player) {
   6620           player = new VideoPlayer();
   6621         }
   6622 
   6623         return player;
   6624       }
   6625     };
   6626   })();
   6627 
   6628   var videoPlayer = YouTubePlayer.getPlayer();
   6629 
   6630   window.onYouTubeIframeAPIReady = function() {
   6631     videoPlayer.isLoaded = true;
   6632 
   6633     if (videoPlayer.queueVideo) {
   6634       videoPlayer.startYouTubePlayer(videoPlayer.queueVideo);
   6635     }
   6636   };
   6637 
   6638   function wrapLinkInPlayer(e) {
   6639     e.preventDefault();
   6640 
   6641     if (!videoPlayer.doneSetup) {
   6642       videoPlayer.setup();
   6643     }
   6644 
   6645     var videoIdMatches = $(e.currentTarget).attr('href').match(/(?:youtu.be\/|v=)([^&]*)/);
   6646     var videoId = videoIdMatches && videoIdMatches[1];
   6647 
   6648     if (videoId) {
   6649       videoPlayer.startYouTubePlayer(videoId);
   6650     }
   6651   }
   6652 
   6653   $(document).on('click.video', 'a[href*="youtube.com/watch"], a[href*="youtu.be"]', wrapLinkInPlayer);
   6654 })(jQuery, window);
   6655 
   6656 /**
   6657  * Wide table
   6658  *
   6659  * Wraps tables in a scrollable area so you can read them on mobile.
   6660  */
   6661 (function($) {
   6662   function initWideTable() {
   6663     $('table.jd-sumtable').each(function(i, table) {
   6664       $(table).wrap('<div class="dac-expand wide-table">');
   6665     });
   6666   }
   6667 
   6668   $(function() {
   6669     initWideTable();
   6670   });
   6671 })(jQuery);
   6672 
   6673 /** Utilities */
   6674 
   6675 /* returns the given string with all HTML brackets converted to entities
   6676     TODO: move this to the site's JS library */
   6677 function escapeHTML(string) {
   6678   return string.replace(/</g,"&lt;")
   6679                 .replace(/>/g,"&gt;");
   6680 };
   6681 
   6682 function getQueryVariable(variable) {
   6683   var query = window.location.search.substring(1);
   6684   var vars = query.split("&");
   6685   for (var i=0;i<vars.length;i++) {
   6686     var pair = vars[i].split("=");
   6687     if(pair[0] == variable){return pair[1];}
   6688   }
   6689   return(false);
   6690 };
   6691