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