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