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