docs.js revision a0a6d3220f83251ae632fa4ed6555bb9040af3fa
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    }
4451    return $(selected);
4452  };
4453})(jQuery);
4454
4455(function($) {
4456  'use strict';
4457
4458  /**
4459   * Toggle the visabilty of the mobile navigation.
4460   * @param {HTMLElement} el - The DOM element.
4461   * @param {Object} options
4462   * @constructor
4463   */
4464  function ToggleNav(el, options) {
4465    this.el = $(el);
4466    this.options = $.extend({}, ToggleNav.DEFAULTS_, options);
4467    this.body = $(document.body);
4468    this.navigation_ = this.body.find(this.options.navigation);
4469    this.el.on('click', this.clickHandler_.bind(this));
4470  }
4471
4472  ToggleNav.BREAKPOINT_ = 980;
4473
4474  /**
4475   * Open on correct sizes
4476   */
4477  function toggleSidebarVisibility(body) {
4478    var wasClosed = ('' + localStorage.getItem('navigation-open')) === 'false';
4479
4480    if (wasClosed) {
4481      body.removeClass(ToggleNav.DEFAULTS_.activeClass);
4482    } else if (window.innerWidth >= ToggleNav.BREAKPOINT_) {
4483      body.addClass(ToggleNav.DEFAULTS_.activeClass);
4484    } else {
4485      body.removeClass(ToggleNav.DEFAULTS_.activeClass);
4486    }
4487  }
4488
4489  /**
4490   * ToggleNav Default Settings
4491   * @type {{body: boolean, dimmer: string, navigation: string, activeClass: string}}
4492   * @private
4493   */
4494  ToggleNav.DEFAULTS_ = {
4495    body: true,
4496    dimmer: '.dac-nav-dimmer',
4497    animatingClass: 'dac-nav-animating',
4498    navigation: '[data-dac-nav]',
4499    activeClass: 'dac-nav-open'
4500  };
4501
4502  /**
4503   * The actual toggle logic.
4504   * @param {Event} event
4505   * @private
4506   */
4507  ToggleNav.prototype.clickHandler_ = function(event) {
4508    event.preventDefault();
4509    var animatingClass = this.options.animatingClass;
4510    var body = this.body;
4511
4512    body.addClass(animatingClass);
4513    body.toggleClass(this.options.activeClass);
4514
4515    setTimeout(function() {
4516      body.removeClass(animatingClass);
4517    }, this.navigation_.transitionDuration());
4518
4519    if (window.innerWidth >= ToggleNav.BREAKPOINT_) {
4520      localStorage.setItem('navigation-open', body.hasClass(this.options.activeClass));
4521    }
4522  };
4523
4524  /**
4525   * jQuery plugin
4526   * @param  {object} options - Override default options.
4527   */
4528  $.fn.dacToggleMobileNav = function() {
4529    return this.each(function() {
4530      var el = $(this);
4531      new ToggleNav(el, el.data());
4532    });
4533  };
4534
4535  $.fn.dacSidebarToggle = function(body) {
4536    toggleSidebarVisibility(body);
4537    $(window).on('resize', toggleSidebarVisibility.bind(null, body));
4538  };
4539
4540  /**
4541   * Data Attribute API
4542   */
4543  $(function() {
4544    $('[data-dac-toggle-nav]').dacToggleMobileNav();
4545  });
4546})(jQuery);
4547
4548(function($) {
4549  'use strict';
4550
4551  /**
4552   * Submit the newsletter form to a Google Form.
4553   * @param {HTMLElement} el - The Form DOM element.
4554   * @constructor
4555   */
4556  function NewsletterForm(el) {
4557    this.el = $(el);
4558    this.form = this.el.find('form');
4559    $('<iframe/>').hide()
4560      .attr('name', 'dac-newsletter-iframe')
4561      .attr('src', '')
4562      .insertBefore(this.form);
4563    this.el.find('[data-newsletter-language]').val(window.polyglot.t('newsletter.languageVal'));
4564    this.form.on('submit', this.submitHandler_.bind(this));
4565  }
4566
4567  /**
4568   * Milliseconds until modal has vanished after modal-close is triggered.
4569   * @type {number}
4570   * @private
4571   */
4572  NewsletterForm.CLOSE_DELAY_ = 300;
4573
4574  /**
4575   * Switch view to display form after close.
4576   * @private
4577   */
4578  NewsletterForm.prototype.closeHandler_ = function() {
4579    setTimeout(function() {
4580      this.el.trigger('swap-reset');
4581    }.bind(this), NewsletterForm.CLOSE_DELAY_);
4582  };
4583
4584  /**
4585   * Reset the modal to initial state.
4586   * @private
4587   */
4588  NewsletterForm.prototype.reset_ = function() {
4589    this.form.trigger('reset');
4590    this.el.one('modal-close', this.closeHandler_.bind(this));
4591  };
4592
4593  /**
4594   * Display a success view on submit.
4595   * @private
4596   */
4597  NewsletterForm.prototype.submitHandler_ = function() {
4598    this.el.one('swap-complete', this.reset_.bind(this));
4599    this.el.trigger('swap-content');
4600  };
4601
4602  /**
4603   * jQuery plugin
4604   * @param  {object} options - Override default options.
4605   */
4606  $.fn.dacNewsletterForm = function(options) {
4607    return this.each(function() {
4608      new NewsletterForm(this, options);
4609    });
4610  };
4611
4612  /**
4613   * Data Attribute API
4614   */
4615  $(document).on('ready.aranja', function() {
4616    $('[data-newsletter]').each(function() {
4617      $(this).dacNewsletterForm();
4618    });
4619  });
4620})(jQuery);
4621
4622/* globals METADATA, YOUTUBE_RESOURCES, BLOGGER_RESOURCES */
4623window.metadata = {};
4624
4625/**
4626 * Prepare metadata and indices for querying.
4627 */
4628window.metadata.prepare = (function() {
4629  // Helper functions.
4630  function mergeArrays() {
4631    return Array.prototype.concat.apply([], arguments);
4632  }
4633
4634  /**
4635   * Creates lookup maps for a resource index.
4636   * I.e. where MAP['some tag'][resource.id] === true when that resource has 'some tag'.
4637   * @param resourceDict
4638   * @returns {{}}
4639   */
4640  function buildResourceLookupMap(resourceDict) {
4641    var map = {};
4642    for (var key in resourceDict) {
4643      var dictForKey = {};
4644      var srcArr = resourceDict[key];
4645      for (var i = 0; i < srcArr.length; i++) {
4646        dictForKey[srcArr[i].index] = true;
4647      }
4648      map[key] = dictForKey;
4649    }
4650    return map;
4651  }
4652
4653  /**
4654   * Merges metadata maps for english and the current language into the global store.
4655   */
4656  function mergeMetadataMap(name, locale) {
4657    if (locale && locale !== 'en' && METADATA[locale]) {
4658      METADATA[name] = $.extend(METADATA.en[name], METADATA[locale][name]);
4659    } else {
4660      METADATA[name] = METADATA.en[name];
4661    }
4662  }
4663
4664  /**
4665   * Index all resources by type, url, tag and category.
4666   * @param resources
4667   */
4668  function createIndices(resources) {
4669    // URL, type, tag and category lookups
4670    var byType = METADATA.byType = {};
4671    var byUrl = METADATA.byUrl = {};
4672    var byTag = METADATA.byTag = {};
4673    var byCategory = METADATA.byCategory = {};
4674
4675    for (var i = 0; i < resources.length; i++) {
4676      var res = resources[i];
4677
4678      // Store index.
4679      res.index = i;
4680
4681      // Index by type.
4682      var type = res.type;
4683      if (type) {
4684        byType[type] = byType[type] || [];
4685        byType[type].push(res);
4686      }
4687
4688      // Index by tag.
4689      var tags = res.tags || [];
4690      for (var j = 0; j < tags.length; j++) {
4691        var tag = tags[j];
4692        if (tag) {
4693          byTag[tag] = byTag[tag] || [];
4694          byTag[tag].push(res);
4695        }
4696      }
4697
4698      // Index by category.
4699      var category = res.category;
4700      if (category) {
4701        byCategory[category] = byCategory[category] || [];
4702        byCategory[category].push(res);
4703      }
4704
4705      // Index by url.
4706      var url = res.url;
4707      if (url) {
4708        res.baseUrl = url.replace(/^intl\/\w+[\/]/, '');
4709        byUrl[res.baseUrl] = res;
4710      }
4711    }
4712    METADATA.hasType = buildResourceLookupMap(byType);
4713    METADATA.hasTag = buildResourceLookupMap(byTag);
4714    METADATA.hasCategory = buildResourceLookupMap(byCategory);
4715  }
4716
4717  return function() {
4718    // Only once.
4719    if (METADATA.all) { return; }
4720
4721    // Get current language.
4722    var locale = getLangPref();
4723
4724    // Merge english resources.
4725    METADATA.all = mergeArrays(
4726      METADATA.en.about,
4727      METADATA.en.design,
4728      METADATA.en.distribute,
4729      METADATA.en.develop,
4730      YOUTUBE_RESOURCES,
4731      BLOGGER_RESOURCES,
4732      METADATA.en.extras
4733    );
4734
4735    // Merge local language resources.
4736    if (locale !== 'en' && METADATA[locale]) {
4737      METADATA.all = mergeArrays(
4738        METADATA.all,
4739        METADATA[locale].about,
4740        METADATA[locale].design,
4741        METADATA[locale].distribute,
4742        METADATA[locale].develop,
4743        METADATA[locale].extras
4744      );
4745    }
4746
4747    mergeMetadataMap('collections', locale);
4748    mergeMetadataMap('searchHeroCollections', locale);
4749    mergeMetadataMap('carousel', locale);
4750
4751    // Create query indicies for resources.
4752    createIndices(METADATA.all, locale);
4753
4754    // Reference metadata.
4755    METADATA.androidReference = window.DATA;
4756    METADATA.googleReference = mergeArrays(window.GMS_DATA, window.GCM_DATA);
4757  };
4758})();
4759
4760/* global METADATA, util */
4761window.metadata.query = (function($) {
4762  var pageMap = {};
4763
4764  function buildResourceList(opts) {
4765    window.metadata.prepare();
4766    var expressions = parseResourceQuery(opts.query || '');
4767    var instanceMap = {};
4768    var results = [];
4769
4770    for (var i = 0; i < expressions.length; i++) {
4771      var clauses = expressions[i];
4772
4773      // Get all resources for first clause
4774      var resources = getResourcesForClause(clauses.shift());
4775
4776      // Concat to final results list
4777      results = results.concat(resources.map(filterResources(clauses, i > 0, instanceMap)).filter(filterEmpty));
4778    }
4779
4780    // Set correct order
4781    if (opts.sortOrder && results.length) {
4782      results = opts.sortOrder === 'random' ? util.shuffle(results) : results.sort(sortResultsByKey(opts.sortOrder));
4783    }
4784
4785    // Slice max results.
4786    if (opts.maxResults !== Infinity) {
4787      results = results.slice(0, opts.maxResults);
4788    }
4789
4790    // Remove page level duplicates
4791    if (opts.allowDuplicates === undefined || opts.allowDuplicates === 'false') {
4792      results = results.filter(removePageLevelDuplicates);
4793
4794      for (var index = 0; index < results.length; ++index) {
4795        pageMap[results[index].index] = 1;
4796      }
4797    }
4798
4799    return results;
4800  }
4801
4802  function filterResources(clauses, removeDuplicates, map) {
4803    return function(resource) {
4804      var resourceIsAllowed = true;
4805
4806      // References must be defined.
4807      if (resource === undefined) {
4808        return;
4809      }
4810
4811      // Get canonical (localized) version of resource if possible.
4812      resource = METADATA.byUrl[resource.baseUrl] || METADATA.byUrl[resource.url] || resource;
4813
4814      // Filter out resources already used
4815      if (removeDuplicates) {
4816        resourceIsAllowed = !map[resource.index];
4817      }
4818
4819      // Must fulfill all criteria
4820      if (clauses.length > 0) {
4821        resourceIsAllowed = resourceIsAllowed && doesResourceMatchClauses(resource, clauses);
4822      }
4823
4824      // Mark resource as used.
4825      if (resourceIsAllowed) {
4826        map[resource.index] = 1;
4827      }
4828
4829      return resourceIsAllowed && resource;
4830    };
4831  }
4832
4833  function filterEmpty(resource) {
4834    return resource;
4835  }
4836
4837  function sortResultsByKey(key) {
4838    var desc = key.charAt(0) === '-';
4839
4840    if (desc) {
4841      key = key.substring(1);
4842    }
4843
4844    return function(x, y) {
4845      return (desc ? -1 : 1) * (parseInt(x[key], 10) - parseInt(y[key], 10));
4846    };
4847  }
4848
4849  function getResourcesForClause(clause) {
4850    switch (clause.attr) {
4851      case 'type':
4852        return METADATA.byType[clause.value];
4853      case 'tag':
4854        return METADATA.byTag[clause.value];
4855      case 'collection':
4856        var resources = METADATA.collections[clause.value] || {};
4857        return getResourcesByUrlCollection(resources.resources);
4858      case 'history':
4859        return getResourcesByUrlCollection($.dacGetVisitedUrls(clause.value));
4860      case 'section':
4861        return getResourcesByUrlCollection([clause.value].sections);
4862      default:
4863        return [];
4864    }
4865  }
4866
4867  function getResourcesByUrlCollection(resources) {
4868    return (resources || []).map(function(url) {
4869      return METADATA.byUrl[url];
4870    });
4871  }
4872
4873  function removePageLevelDuplicates(resource) {
4874    return resource && !pageMap[resource.index];
4875  }
4876
4877  function doesResourceMatchClauses(resource, clauses) {
4878    for (var i = 0; i < clauses.length; i++) {
4879      var map;
4880      switch (clauses[i].attr) {
4881        case 'type':
4882          map = METADATA.hasType[clauses[i].value];
4883          break;
4884        case 'tag':
4885          map = METADATA.hasTag[clauses[i].value];
4886          break;
4887      }
4888
4889      if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) {
4890        return clauses[i].negative;
4891      }
4892    }
4893
4894    return true;
4895  }
4896
4897  function parseResourceQuery(query) {
4898    // Parse query into array of expressions (expression e.g. 'tag:foo + type:video')
4899    var expressions = [];
4900    var expressionStrs = query.split(',') || [];
4901    for (var i = 0; i < expressionStrs.length; i++) {
4902      var expr = expressionStrs[i] || '';
4903
4904      // Break expression into clauses (clause e.g. 'tag:foo')
4905      var clauses = [];
4906      var clauseStrs = expr.split(/(?=[\+\-])/);
4907      for (var j = 0; j < clauseStrs.length; j++) {
4908        var clauseStr = clauseStrs[j] || '';
4909
4910        // Get attribute and value from clause (e.g. attribute='tag', value='foo')
4911        var parts = clauseStr.split(':');
4912        var clause = {};
4913
4914        clause.attr = parts[0].replace(/^\s+|\s+$/g, '');
4915        if (clause.attr) {
4916          if (clause.attr.charAt(0) === '+') {
4917            clause.attr = clause.attr.substring(1);
4918          } else if (clause.attr.charAt(0) === '-') {
4919            clause.negative = true;
4920            clause.attr = clause.attr.substring(1);
4921          }
4922        }
4923
4924        if (parts.length > 1) {
4925          clause.value = parts[1].replace(/^\s+|\s+$/g, '');
4926        }
4927
4928        clauses.push(clause);
4929      }
4930
4931      if (!clauses.length) {
4932        continue;
4933      }
4934
4935      expressions.push(clauses);
4936    }
4937
4938    return expressions;
4939  }
4940
4941  return buildResourceList;
4942})(jQuery);
4943
4944/* global METADATA, getLangPref */
4945
4946window.metadata.search = (function() {
4947  'use strict';
4948
4949  var currentLang = getLangPref();
4950
4951  function search(query) {
4952    window.metadata.prepare();
4953    return {
4954      android: findDocsMatches(query, METADATA.androidReference),
4955      docs: findDocsMatches(query, METADATA.googleReference),
4956      resources: findResourceMatches(query)
4957    };
4958  }
4959
4960  function findDocsMatches(query, data) {
4961    var results = [];
4962
4963    for (var i = 0; i < data.length; i++) {
4964      var s = data[i];
4965      if (query.length !== 0 && s.label.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
4966        results.push(s);
4967      }
4968    }
4969
4970    rankAutocompleteApiResults(query, results);
4971
4972    return results;
4973  }
4974
4975  function findResourceMatches(query) {
4976    var results = [];
4977
4978    // Search for matching JD docs
4979    if (query.length >= 2) {
4980      /* In some langs, spaces may be optional between certain non-Ascii word-glyphs. For
4981       * those langs, only match query at word boundaries if query includes Ascii chars only.
4982       */
4983      var NO_BOUNDARY_LANGUAGES = ['ja','ko','vi','zh-cn','zh-tw'];
4984      var isAsciiOnly = /^[\u0000-\u007f]*$/.test(query);
4985      var noBoundaries = (NO_BOUNDARY_LANGUAGES.indexOf(window.getLangPref()) !== -1);
4986      var exprBoundary = (!isAsciiOnly && noBoundaries) ? '' : '(?:^|\\s)';
4987      var queryRegex = new RegExp(exprBoundary + query.toLowerCase(), 'g');
4988
4989      var all = METADATA.all;
4990      for (var i = 0; i < all.length; i++) {
4991        // current search comparison, with counters for tag and title,
4992        // used later to improve ranking
4993        var s = all[i];
4994        s.matched_tag = 0;
4995        s.matched_title = 0;
4996        var matched = false;
4997
4998        // Check if query matches any tags; work backwards toward 1 to assist ranking
4999        if (s.keywords) {
5000          for (var j = s.keywords.length - 1; j >= 0; j--) {
5001            // it matches a tag
5002            if (s.keywords[j].toLowerCase().match(queryRegex)) {
5003              matched = true;
5004              s.matched_tag = j + 1; // add 1 to index position
5005            }
5006          }
5007        }
5008
5009        // Check if query matches doc title
5010        if (s.title.toLowerCase().match(queryRegex)) {
5011          matched = true;
5012          s.matched_title = 1;
5013        }
5014
5015        // Remember the doc if it matches either
5016        if (matched) {
5017          results.push(s);
5018        }
5019      }
5020
5021      // Improve the current results
5022      results = lookupBetterResult(results);
5023
5024      // Rank/sort all the matched pages
5025      rankAutocompleteDocResults(results);
5026
5027      return results;
5028    }
5029  }
5030
5031  // Replaces a match with another resource by url, if it exists.
5032  function lookupReplacementByUrl(match, url) {
5033    var replacement = METADATA.byUrl[url];
5034
5035    // Replacement resource does not exists.
5036    if (!replacement) { return; }
5037
5038    replacement.matched_title = Math.max(replacement.matched_title, match.matched_title);
5039    replacement.matched_tag = Math.max(replacement.matched_tag, match.matched_tag);
5040
5041    return replacement;
5042  }
5043
5044  // Find the localized version of a page if it exists.
5045  function lookupLocalizedVersion(match) {
5046    return METADATA.byUrl[match.baseUrl] || METADATA.byUrl[match.url];
5047  }
5048
5049  // Find the main page for a tutorial when matching a subpage.
5050  function lookupTutorialIndex(match) {
5051    // Guard for non index tutorial pages.
5052    if (match.type !== 'training' || match.url.indexOf('index.html') >= 0) { return; }
5053
5054    var indexUrl = match.url.replace(/[^\/]+$/, 'index.html');
5055    return lookupReplacementByUrl(match, indexUrl);
5056  }
5057
5058  // Find related results which are a better match for the user.
5059  function lookupBetterResult(matches) {
5060    var newMatches = [];
5061
5062    matches = matches.filter(function(match) {
5063      var newMatch = match;
5064      newMatch = lookupTutorialIndex(newMatch) || newMatch;
5065      newMatch = lookupLocalizedVersion(newMatch) || newMatch;
5066
5067      if (newMatch !== match) {
5068        newMatches.push(newMatch);
5069      }
5070
5071      return newMatch === match;
5072    });
5073
5074    return toUnique(newMatches.concat(matches));
5075  }
5076
5077  /* Order the jd doc result list based on match quality */
5078  function rankAutocompleteDocResults(matches) {
5079    if (!matches || !matches.length) {
5080      return;
5081    }
5082
5083    var _resultScoreFn = function(match) {
5084      var score = 1.0;
5085
5086      // if the query matched a tag
5087      if (match.matched_tag > 0) {
5088        // multiply score by factor relative to position in tags list (max of 3)
5089        score *= 3 / match.matched_tag;
5090
5091        // if it also matched the title
5092        if (match.matched_title > 0) {
5093          score *= 2;
5094        }
5095      } else if (match.matched_title > 0) {
5096        score *= 3;
5097      }
5098
5099      if (match.lang === currentLang) {
5100        score *= 5;
5101      }
5102
5103      return score;
5104    };
5105
5106    for (var i = 0; i < matches.length; i++) {
5107      matches[i].__resultScore = _resultScoreFn(matches[i]);
5108    }
5109
5110    matches.sort(function(a, b) {
5111      var n = b.__resultScore - a.__resultScore;
5112
5113      if (n === 0) {
5114        // lexicographical sort if scores are the same
5115        n = (a.title < b.title) ? -1 : 1;
5116      }
5117
5118      return n;
5119    });
5120  }
5121
5122  /* Order the result list based on match quality */
5123  function rankAutocompleteApiResults(query, matches) {
5124    query = query || '';
5125    if (!matches || !matches.length) {
5126      return;
5127    }
5128
5129    // helper function that gets the last occurence index of the given regex
5130    // in the given string, or -1 if not found
5131    var _lastSearch = function(s, re) {
5132      if (s === '') {
5133        return -1;
5134      }
5135      var l = -1;
5136      var tmp;
5137      while ((tmp = s.search(re)) >= 0) {
5138        if (l < 0) {
5139          l = 0;
5140        }
5141        l += tmp;
5142        s = s.substr(tmp + 1);
5143      }
5144      return l;
5145    };
5146
5147    // helper function that counts the occurrences of a given character in
5148    // a given string
5149    var _countChar = function(s, c) {
5150      var n = 0;
5151      for (var i = 0; i < s.length; i++) {
5152        if (s.charAt(i) === c) {
5153          ++n;
5154        }
5155      }
5156      return n;
5157    };
5158
5159    var queryLower = query.toLowerCase();
5160    var queryAlnum = (queryLower.match(/\w+/) || [''])[0];
5161    var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum);
5162    var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b');
5163
5164    var _resultScoreFn = function(result) {
5165      // scores are calculated based on exact and prefix matches,
5166      // and then number of path separators (dots) from the last
5167      // match (i.e. favoring classes and deep package names)
5168      var score = 1.0;
5169      var labelLower = result.label.toLowerCase();
5170      var t;
5171      var partsAfter;
5172      t = _lastSearch(labelLower, partExactAlnumRE);
5173      if (t >= 0) {
5174        // exact part match
5175        partsAfter = _countChar(labelLower.substr(t + 1), '.');
5176        score *= 200 / (partsAfter + 1);
5177      } else {
5178        t = _lastSearch(labelLower, partPrefixAlnumRE);
5179        if (t >= 0) {
5180          // part prefix match
5181          partsAfter = _countChar(labelLower.substr(t + 1), '.');
5182          score *= 20 / (partsAfter + 1);
5183        }
5184      }
5185
5186      return score;
5187    };
5188
5189    for (var i = 0; i < matches.length; i++) {
5190      // if the API is deprecated, default score is 0; otherwise, perform scoring
5191      if (matches[i].deprecated === 'true') {
5192        matches[i].__resultScore = 0;
5193      } else {
5194        matches[i].__resultScore = _resultScoreFn(matches[i]);
5195      }
5196    }
5197
5198    matches.sort(function(a, b) {
5199      var n = b.__resultScore - a.__resultScore;
5200
5201      if (n === 0) {
5202        // lexicographical sort if scores are the same
5203        n = (a.label < b.label) ? -1 : 1;
5204      }
5205
5206      return n;
5207    });
5208  }
5209
5210  // Destructive but fast toUnique.
5211  // http://stackoverflow.com/a/25082874
5212  function toUnique(array) {
5213    var c;
5214    var b = array.length || 1;
5215
5216    while (c = --b) {
5217      while (c--) {
5218        if (array[b] === array[c]) {
5219          array.splice(c, 1);
5220        }
5221      }
5222    }
5223    return array;
5224  }
5225
5226  return search;
5227})();
5228
5229(function($) {
5230  'use strict';
5231
5232  /**
5233   * Smoothly scroll to location on current page.
5234   * @param el
5235   * @param options
5236   * @constructor
5237   */
5238  function ScrollButton(el, options) {
5239    this.el = $(el);
5240    this.target = $(this.el.attr('href'));
5241    this.options = $.extend({}, ScrollButton.DEFAULTS_, options);
5242
5243    if (typeof this.options.offset === 'string') {
5244      this.options.offset = $(this.options.offset).height();
5245    }
5246
5247    this.el.on('click', this.clickHandler_.bind(this));
5248  }
5249
5250  /**
5251   * Default options
5252   * @type {{duration: number, easing: string, offset: number, scrollContainer: string}}
5253   * @private
5254   */
5255  ScrollButton.DEFAULTS_ = {
5256    duration: 300,
5257    easing: 'swing',
5258    offset: '.dac-header',
5259    scrollContainer: 'html, body'
5260  };
5261
5262  /**
5263   * Scroll logic
5264   * @param event
5265   * @private
5266   */
5267  ScrollButton.prototype.clickHandler_ = function(event) {
5268    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
5269      return;
5270    }
5271
5272    event.preventDefault();
5273
5274    var position = this.getTargetPosition();
5275    $(this.options.scrollContainer).animate({
5276      scrollTop: position - this.options.offset
5277    }, this.options);
5278  };
5279
5280  ScrollButton.prototype.getTargetPosition = function() {
5281    if (this.options.scrollContainer === ScrollButton.DEFAULTS_.scrollContainer) {
5282      return this.target.offset().top;
5283    }
5284    var scrollContainer = $(this.options.scrollContainer)[0];
5285    var currentEl = this.target[0];
5286    var pos = 0;
5287    while (currentEl !== scrollContainer && currentEl !== null) {
5288      pos += currentEl.offsetTop;
5289      currentEl = currentEl.offsetParent;
5290    }
5291    return pos;
5292  };
5293
5294  /**
5295   * jQuery plugin
5296   * @param  {object} options - Override default options.
5297   */
5298  $.fn.dacScrollButton = function(options) {
5299    return this.each(function() {
5300      new ScrollButton(this, options);
5301    });
5302  };
5303
5304  /**
5305   * Data Attribute API
5306   */
5307  $(document).on('ready.aranja', function() {
5308    $('[data-scroll-button]').each(function() {
5309      $(this).dacScrollButton($(this).data());
5310    });
5311  });
5312})(jQuery);
5313
5314/* global getLangPref */
5315(function($) {
5316  var LANG;
5317
5318  function getSearchLang() {
5319    if (!LANG) {
5320      LANG = getLangPref();
5321
5322      // Fix zh-cn to be zh-CN.
5323      LANG = LANG.replace(/-\w+/, function(m) { return m.toUpperCase(); });
5324    }
5325    return LANG;
5326  }
5327
5328  function customSearch(query, start) {
5329    var searchParams = {
5330      // current cse instance:
5331      //cx: '001482626316274216503:zu90b7s047u',
5332      // new cse instance:
5333      cx: '000521750095050289010:zpcpi1ea4s8',
5334      key: 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8',
5335      q: query,
5336      start: start || 1,
5337      num: 6,
5338      hl: getSearchLang(),
5339      fields: 'queries,items(pagemap,link,title,htmlSnippet,formattedUrl)'
5340    };
5341
5342    return $.get('https://content.googleapis.com/customsearch/v1?' +  $.param(searchParams));
5343  }
5344
5345  function renderResults(el, results) {
5346    if (!results.items) {
5347      el.append($('<div>').text('No results'));
5348      return;
5349    }
5350
5351    for (var i = 0; i < results.items.length; i++) {
5352      var item = results.items[i];
5353      var hasImage = item.pagemap && item.pagemap.cse_thumbnail;
5354      var sectionMatch = item.link.match(/developer\.android\.com\/(\w*)/);
5355      var section = (sectionMatch && sectionMatch[1]) || 'blog';
5356
5357      var entry = $('<div>').addClass('dac-custom-search-entry cols');
5358
5359      if (hasImage) {
5360        var image = item.pagemap.cse_thumbnail[0];
5361        entry.append($('<div>').addClass('col-1of6')
5362          .append($('<div>').addClass('dac-custom-search-image').css('background-image', 'url(' + image.src + ')')));
5363      }
5364
5365      entry.append($('<div>').addClass(hasImage ? 'col-5of6' : 'col-6of6')
5366        .append($('<p>').addClass('dac-custom-search-section').text(section))
5367        .append(
5368          $('<a>').text(item.title).attr('href', item.link).wrap('<h2>').parent().addClass('dac-custom-search-title')
5369        )
5370        .append($('<p>').addClass('dac-custom-search-snippet').html(item.htmlSnippet.replace(/<br>/g, '')))
5371        .append($('<a>').addClass('dac-custom-search-link').text(item.formattedUrl).attr('href', item.link)));
5372
5373      el.append(entry);
5374    }
5375
5376    if (results.queries.nextPage) {
5377      var loadMoreButton = $('<button id="dac-custom-search-load-more">')
5378        .addClass('dac-custom-search-load-more')
5379        .text('Load more')
5380        .click(function() {
5381          loadMoreResults(el, results);
5382        });
5383
5384      el.append(loadMoreButton);
5385    }
5386  }
5387
5388  function loadMoreResults(el, results) {
5389    var query = results.queries.request.searchTerms;
5390    var start = results.queries.nextPage.startIndex;
5391    var loadMoreButton = el.find('#dac-custom-search-load-more');
5392
5393    loadMoreButton.text('Loading more...');
5394
5395    customSearch(query, start).then(function(results) {
5396      loadMoreButton.remove();
5397      renderResults(el, results);
5398    });
5399  }
5400
5401  $.fn.customSearch = function(query) {
5402    var el = $(this);
5403
5404    customSearch(query).then(function(results) {
5405      el.empty();
5406      renderResults(el, results);
5407    });
5408  };
5409})(jQuery);
5410
5411/* global METADATA */
5412
5413(function($) {
5414  $.fn.dacSearchRenderHero = function(resources, query) {
5415    var el = $(this);
5416    el.empty();
5417
5418    var resource = METADATA.searchHeroCollections[query];
5419
5420    if (resource) {
5421      el.dacHero(resource, true);
5422      el.show();
5423
5424      return true;
5425    } else {
5426      el.hide();
5427    }
5428  };
5429})(jQuery);
5430
5431(function($) {
5432  $.fn.dacSearchRenderReferences = function(results, query) {
5433    var referenceCard = $('.suggest-card.reference');
5434    referenceCard.data('searchreferences.dac', {results: results, query: query});
5435    renderResults(referenceCard, results, query, false);
5436  };
5437
5438  var ROW_COUNT_COLLAPSED = 7;
5439  var ROW_COUNT_EXPANDED = 33;
5440  var ROW_COUNT_GOOGLE_COLLAPSED = 1;
5441  var ROW_COUNT_GOOGLE_EXPANDED = 8;
5442
5443  function onSuggestionClick(e) {
5444    var normalClick = e.which === 1 && !e.ctrlKey && !e.shiftKey && !e.metaKey;
5445    if (normalClick) {
5446      e.preventDefault();
5447    }
5448
5449    // When user clicks a suggested document, track it
5450    var url = $(e.currentTarget).attr('href');
5451    ga('send', 'event', 'Suggestion Click', 'clicked: ' + url,
5452        'query: ' + $('#search_autocomplete').val().toLowerCase(),
5453        {hitCallback: function() {
5454          if (normalClick) {
5455            document.location = url;
5456          }
5457        }});
5458  }
5459
5460  function buildLink(match) {
5461    var link = $('<a>').attr('href', window.toRoot + match.link);
5462
5463    var label = match.label;
5464    var classNameStart = label.match(/[A-Z]/) ? label.search(/[A-Z]/) : label.lastIndexOf('.') + 1;
5465
5466    var newLink = '<span class="namespace">' +
5467      label.substr(0, classNameStart) +
5468      '</span><br />' +
5469      label.substr(classNameStart, label.length);
5470
5471    link.html(newLink);
5472    return link;
5473  }
5474
5475  function buildSuggestion(match, query) {
5476    var li = $('<li>').addClass('dac-search-results-reference-entry');
5477
5478    var link = buildLink(match);
5479    link.highlightMatches(query);
5480    li.append(link);
5481    return li[0];
5482  }
5483
5484  function buildResults(results, query) {
5485    return results.map(function(match) {
5486      return buildSuggestion(match, query);
5487    });
5488  }
5489
5490  function renderAndroidResults(list, gMatches, query) {
5491    list.empty();
5492
5493    var header = $('<li class="dac-search-results-reference-header">Reference</li>');
5494    list.append(header);
5495
5496    if (gMatches.length > 0) {
5497      list.removeClass('no-results');
5498
5499      var resources = buildResults(gMatches, query);
5500      list.append(resources);
5501
5502      return true;
5503    } else {
5504      list.append('<li class="dac-search-results-reference-entry-empty">No results</li>');
5505    }
5506  }
5507
5508  function renderGoogleDocsResults(list, gGoogleMatches, query) {
5509    list = $('.suggest-card.reference ul');
5510
5511    if (gGoogleMatches.length > 0) {
5512      list.append('<li class="dac-search-results-reference-header">in Google Services</li>');
5513
5514      var resources = buildResults(gGoogleMatches, query);
5515      list.append(resources);
5516
5517      return true;
5518    }
5519  }
5520
5521  function renderResults(referenceCard, results, query, expanded) {
5522    var list = referenceCard.find('ul');
5523    list.toggleClass('is-expanded', !!expanded);
5524
5525    // Figure out how many results we can show in our fixed size box.
5526    var total = expanded ? ROW_COUNT_EXPANDED : ROW_COUNT_COLLAPSED;
5527    var googleCount = expanded ? ROW_COUNT_GOOGLE_EXPANDED : ROW_COUNT_GOOGLE_COLLAPSED;
5528    googleCount = Math.max(googleCount, total - results.android.length);
5529    googleCount = Math.min(googleCount, results.docs.length);
5530
5531    if (googleCount > 0) {
5532      // If there are google results, reserve space for its header.
5533      googleCount++;
5534    }
5535
5536    var androidCount = Math.max(0, total - googleCount);
5537    if (androidCount === 0) {
5538      // Reserve space for "No reference results"
5539      googleCount--;
5540    }
5541
5542    renderAndroidResults(list, results.android.slice(0, androidCount), query);
5543    renderGoogleDocsResults(list, results.docs.slice(0, googleCount - 1), query);
5544
5545    var totalResults = results.android.length + results.docs.length;
5546    if (totalResults === 0) {
5547      list.addClass('no-results');
5548    }
5549
5550    // Tweak see more logic to account for references.
5551    var hasMore = totalResults > ROW_COUNT_COLLAPSED && !util.matchesMedia('mobile');
5552    var searchEl = $('#search-resources');
5553    searchEl.toggleClass('dac-has-more', searchEl.hasClass('dac-has-more') || (hasMore && !expanded));
5554    searchEl.toggleClass('dac-has-less', searchEl.hasClass('dac-has-less') || (hasMore && expanded));
5555  }
5556
5557  function onToggleMore(e) {
5558    var link = $(e.currentTarget);
5559    var referenceCard = $('.suggest-card.reference');
5560    var data = referenceCard.data('searchreferences.dac');
5561
5562    if (util.matchesMedia('mobile')) { return; }
5563
5564    renderResults(referenceCard, data.results, data.query, link.data('toggle') === 'show-more');
5565  }
5566
5567  $(document).on('click', '.dac-search-results-resources [data-toggle="show-more"]', onToggleMore);
5568  $(document).on('click', '.dac-search-results-resources [data-toggle="show-less"]', onToggleMore);
5569  $(document).on('click', '.suggest-card.reference a', onSuggestionClick);
5570})(jQuery);
5571
5572(function($) {
5573  function highlightPage(query, page) {
5574    page.find('.title').highlightMatches(query);
5575  }
5576
5577  $.fn.dacSearchRenderResources = function(gDocsMatches, query) {
5578    this.resourceWidget(gDocsMatches, {
5579      itemsPerPage: 18,
5580      initialResults: 6,
5581      cardSizes: ['6x2'],
5582      onRenderPage: highlightPage.bind(null, query)
5583    });
5584
5585    return this;
5586  };
5587})(jQuery);
5588
5589/*global metadata */
5590
5591(function($, metadata) {
5592  'use strict';
5593
5594  function Search() {
5595    this.body = $('body');
5596    this.lastQuery = null;
5597    this.searchResults = $('#search-results');
5598    this.searchClose = $('[data-search-close]');
5599    this.searchClear = $('[data-search-clear]');
5600    this.searchInput = $('#search_autocomplete');
5601    this.searchResultsContent = $('#dac-search-results-content');
5602    this.searchResultsFor = $('#search-results-for');
5603    this.searchResultsHistory = $('#dac-search-results-history');
5604    this.searchResultsResources = $('#search-resources');
5605    this.searchResultsHero = $('#dac-search-results-hero');
5606    this.searchResultsReference = $('#dac-search-results-reference');
5607    this.searchHeader = $('[data-search]').data('search-input.dac');
5608  }
5609
5610  Search.prototype.init = function() {
5611    if (this.checkRedirectToIndex()) { return; }
5612
5613    this.searchHistory = window.dacStore('search-history');
5614
5615    this.searchInput.focus(this.onSearchChanged.bind(this));
5616    this.searchInput.keydown(this.handleKeyboardShortcut.bind(this));
5617    this.searchInput.on('input', this.onSearchChanged.bind(this));
5618    this.searchClear.click(this.clear.bind(this));
5619    this.searchClose.click(this.close.bind(this));
5620
5621    this.customSearch = $.fn.debounce(function(query) {
5622      $('#dac-custom-search-results').customSearch(query);
5623    }, 1000);
5624
5625    // Start search shortcut (/)
5626    $('body').keyup(function(event) {
5627      if (event.which === 191 && $(event.target).is(':not(:input)')) {
5628        this.searchInput.focus();
5629      }
5630    }.bind(this));
5631
5632    $(window).on('popstate', this.onPopState.bind(this));
5633    $(window).hashchange(this.onHashChange.bind(this));
5634    this.onHashChange();
5635  };
5636
5637  Search.prototype.checkRedirectToIndex = function() {
5638    var query = this.getUrlQuery();
5639    var target = window.getLangTarget();
5640    var prefix = (target !== 'en') ? '/intl/' + target : '';
5641    var pathname = location.pathname.slice(prefix.length);
5642    if (query != null && pathname !== '/index.html') {
5643      location.href = prefix + '/index.html' + location.hash;
5644      return true;
5645    }
5646  };
5647
5648  Search.prototype.handleKeyboardShortcut = function(event) {
5649    // Close (esc)
5650    if (event.which === 27) {
5651      this.searchClose.trigger('click');
5652      event.preventDefault();
5653    }
5654
5655    // Previous result (up arrow)
5656    if (event.which === 38) {
5657      this.previousResult();
5658      event.preventDefault();
5659    }
5660
5661    // Next result (down arrow)
5662    if (event.which === 40) {
5663      this.nextResult();
5664      event.preventDefault();
5665    }
5666
5667    // Navigate to result (enter)
5668    if (event.which === 13) {
5669      this.navigateToResult();
5670      event.preventDefault();
5671    }
5672  };
5673
5674  Search.prototype.goToResult = function(relativeIndex) {
5675    var links = this.searchResults.find('a').filter(':visible');
5676    var selectedLink = this.searchResults.find('.dac-selected');
5677
5678    if (selectedLink.length) {
5679      var found = $.inArray(selectedLink[0], links);
5680
5681      selectedLink.removeClass('dac-selected');
5682      links.eq(found + relativeIndex).addClass('dac-selected');
5683      return true;
5684    } else {
5685      if (relativeIndex > 0) {
5686        links.first().addClass('dac-selected');
5687      }
5688    }
5689  };
5690
5691  Search.prototype.previousResult = function() {
5692    this.goToResult(-1);
5693  };
5694
5695  Search.prototype.nextResult = function() {
5696    this.goToResult(1);
5697  };
5698
5699  Search.prototype.navigateToResult = function() {
5700    var query = this.getQuery();
5701    var selectedLink = this.searchResults.find('.dac-selected');
5702
5703    if (selectedLink.length) {
5704      selectedLink[0].click();
5705    } else {
5706      this.searchHistory.push(query);
5707      this.addQueryToUrl(query);
5708
5709      var isMobileOrTablet = typeof window.orientation !== 'undefined';
5710
5711      if (isMobileOrTablet) {
5712        this.searchInput.blur();
5713      }
5714    }
5715  };
5716
5717  Search.prototype.onHashChange = function() {
5718    var query = this.getUrlQuery();
5719    if (query != null && query !== this.getQuery()) {
5720      this.searchInput.val(query);
5721      this.onSearchChanged();
5722    }
5723  };
5724
5725  Search.prototype.clear = function() {
5726    this.searchInput.val('');
5727    window.location.hash = '';
5728    this.onSearchChanged();
5729    this.searchInput.focus();
5730  };
5731
5732  Search.prototype.close = function() {
5733    this.removeQueryFromUrl();
5734    this.searchInput.blur();
5735    this.hideOverlay();
5736  };
5737
5738  Search.prototype.getUrlQuery = function() {
5739    var queryMatch = location.hash.match(/q=(.*)&?/);
5740    return queryMatch && queryMatch[1] && decodeURI(queryMatch[1]);
5741  };
5742
5743  Search.prototype.getQuery = function() {
5744    return this.searchInput.val().replace(/(^ +)|( +$)/g, '');
5745  };
5746
5747  Search.prototype.onSearchChanged = function() {
5748    var query = this.getQuery();
5749
5750    this.showOverlay();
5751    this.render(query);
5752  };
5753
5754  Search.prototype.render = function(query) {
5755    if (this.lastQuery === query) { return; }
5756
5757    if (query.length < 2) {
5758      query = '';
5759    }
5760
5761    this.lastQuery = query;
5762    this.searchResultsFor.text(query);
5763    this.customSearch(query);
5764    var metadataResults = metadata.search(query);
5765    this.searchResultsResources.dacSearchRenderResources(metadataResults.resources, query);
5766    this.searchResultsReference.dacSearchRenderReferences(metadataResults, query);
5767    var hasHero = this.searchResultsHero.dacSearchRenderHero(metadataResults.resources, query);
5768    var hasQuery = !!query;
5769
5770    this.searchResultsReference.toggle(!hasHero);
5771    this.searchResultsContent.toggle(hasQuery);
5772    this.searchResultsHistory.toggle(!hasQuery);
5773    this.addQueryToUrl(query);
5774    this.pushState();
5775  };
5776
5777  Search.prototype.addQueryToUrl = function(query) {
5778    var hash = 'q=' + encodeURI(query);
5779
5780    if (query) {
5781      if (window.history.replaceState) {
5782        window.history.replaceState(null, '', '#' + hash);
5783      } else {
5784        window.location.hash = hash;
5785      }
5786    }
5787  };
5788
5789  Search.prototype.onPopState = function() {
5790    if (!this.getUrlQuery()) {
5791      this.hideOverlay();
5792      this.searchHeader.unsetActiveState();
5793    }
5794  };
5795
5796  Search.prototype.removeQueryFromUrl = function() {
5797    window.location.hash = '';
5798  };
5799
5800  Search.prototype.pushState = function() {
5801    if (window.history.pushState && !this.lastQuery.length) {
5802      window.history.pushState(null, '');
5803    }
5804  };
5805
5806  Search.prototype.showOverlay = function() {
5807    this.body.addClass('dac-modal-open dac-search-open');
5808  };
5809
5810  Search.prototype.hideOverlay = function() {
5811    this.body.removeClass('dac-modal-open dac-search-open');
5812  };
5813
5814  $(document).on('ready.aranja', function() {
5815    var search = new Search();
5816    search.init();
5817  });
5818})(jQuery, metadata);
5819
5820window.dacStore = (function(window) {
5821  /**
5822   * Creates a new persistent store.
5823   * If localStorage is unavailable, the items are stored in memory.
5824   *
5825   * @constructor
5826   * @param {string} name    The name of the store
5827   * @param {number} maxSize The maximum number of items the store can hold.
5828   */
5829  var Store = function(name, maxSize) {
5830    var content = [];
5831
5832    var hasLocalStorage = !!window.localStorage;
5833
5834    if (hasLocalStorage) {
5835      try {
5836        content = JSON.parse(window.localStorage.getItem(name) || []);
5837      } catch (e) {
5838        // Store contains invalid data
5839        window.localStorage.removeItem(name);
5840      }
5841    }
5842
5843    function push(item) {
5844      if (content[0] === item) {
5845        return;
5846      }
5847
5848      content.unshift(item);
5849
5850      if (maxSize) {
5851        content.splice(maxSize, content.length);
5852      }
5853
5854      if (hasLocalStorage) {
5855        window.localStorage.setItem(name, JSON.stringify(content));
5856      }
5857    }
5858
5859    function all() {
5860      // Return a copy
5861      return content.slice();
5862    }
5863
5864    return {
5865      push: push,
5866      all: all
5867    };
5868  };
5869
5870  var stores = {
5871    'search-history': new Store('search-history', 3)
5872  };
5873
5874  /**
5875   * Get a named persistent store.
5876   * @param  {string} name
5877   * @return {Store}
5878   */
5879  return function getStore(name) {
5880    return stores[name];
5881  };
5882})(window);
5883
5884(function($) {
5885  'use strict';
5886
5887  /**
5888   * A component that swaps two dynamic height views with an animation.
5889   * Listens for the following events:
5890   * * swap-content: triggers SwapContent.swap_()
5891   * * swap-reset: triggers SwapContent.reset()
5892   * @param el
5893   * @param options
5894   * @constructor
5895   */
5896  function SwapContent(el, options) {
5897    this.el = $(el);
5898    this.options = $.extend({}, SwapContent.DEFAULTS_, options);
5899    this.options.dynamic = this.options.dynamic === 'true';
5900    this.containers = this.el.find(this.options.container);
5901    this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0);
5902    this.el.on('swap-content', this.swap.bind(this));
5903    this.el.on('swap-reset', this.reset.bind(this));
5904    this.el.find(this.options.swapButton).on('click', this.swap.bind(this));
5905  }
5906
5907  /**
5908   * SwapContent's default settings.
5909   * @type {{activeClass: string, container: string, transitionSpeed: number}}
5910   * @private
5911   */
5912  SwapContent.DEFAULTS_ = {
5913    activeClass: 'dac-active',
5914    container: '[data-swap-container]',
5915    dynamic: 'true',
5916    swapButton: '[data-swap-button]',
5917    transitionSpeed: 500
5918  };
5919
5920  /**
5921   * Returns container's visible height.
5922   * @param container
5923   * @returns {number}
5924   */
5925  SwapContent.prototype.currentHeight = function(container) {
5926    return container.children('.' + this.options.activeClass).outerHeight();
5927  };
5928
5929  /**
5930   * Reset to show initial content
5931   */
5932  SwapContent.prototype.reset = function() {
5933    if (!this.initiallyActive.hasClass(this.initiallyActive)) {
5934      this.containers.children().toggleClass(this.options.activeClass);
5935    }
5936  };
5937
5938  /**
5939   * Complete the swap.
5940   */
5941  SwapContent.prototype.complete = function() {
5942    this.containers.height('auto');
5943    this.containers.trigger('swap-complete');
5944  };
5945
5946  /**
5947   * Perform the swap of content.
5948   */
5949  SwapContent.prototype.swap = function() {
5950    this.containers.each(function(index, container) {
5951      container = $(container);
5952
5953      if (!this.options.dynamic) {
5954        container.children().toggleClass(this.options.activeClass);
5955        this.complete.bind(this);
5956        return;
5957      }
5958
5959      container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass);
5960      container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed,
5961        this.complete.bind(this));
5962    }.bind(this));
5963  };
5964
5965  /**
5966   * jQuery plugin
5967   * @param  {object} options - Override default options.
5968   */
5969  $.fn.dacSwapContent = function(options) {
5970    return this.each(function() {
5971      new SwapContent(this, options);
5972    });
5973  };
5974
5975  /**
5976   * Data Attribute API
5977   */
5978  $(document).on('ready.aranja', function() {
5979    $('[data-swap]').each(function() {
5980      $(this).dacSwapContent($(this).data());
5981    });
5982  });
5983})(jQuery);
5984
5985/* Tabs */
5986(function($) {
5987  'use strict';
5988
5989  /**
5990   * @param {HTMLElement} el - The DOM element.
5991   * @param {Object} options
5992   * @constructor
5993   */
5994  function Tabs(el, options) {
5995    this.el = $(el);
5996    this.options = $.extend({}, Tabs.DEFAULTS_, options);
5997    this.init();
5998  }
5999
6000  Tabs.DEFAULTS_ = {
6001    activeClass: 'dac-active',
6002    viewDataAttr: 'tab-view',
6003    itemDataAttr: 'tab-item'
6004  };
6005
6006  Tabs.prototype.init = function() {
6007    var itemDataAttribute = '[data-' + this.options.itemDataAttr + ']';
6008    this.tabEl_ = this.el.find(itemDataAttribute);
6009    this.tabViewEl_ = this.el.find('[data-' + this.options.viewDataAttr + ']');
6010    this.el.on('click.dac-tabs', itemDataAttribute, this.changeTabs.bind(this));
6011  };
6012
6013  Tabs.prototype.changeTabs = function(event) {
6014    var current = $(event.currentTarget);
6015    var index = current.index();
6016
6017    if (current.hasClass(this.options.activeClass)) {
6018      current.add(this.tabViewEl_.eq(index)).removeClass(this.options.activeClass);
6019    } else {
6020      this.tabEl_.add(this.tabViewEl_).removeClass(this.options.activeClass);
6021      current.add(this.tabViewEl_.eq(index)).addClass(this.options.activeClass);
6022    }
6023  };
6024
6025  /**
6026   * jQuery plugin
6027   */
6028  $.fn.dacTabs = function() {
6029    return this.each(function() {
6030      var el = $(this);
6031      new Tabs(el, el.data());
6032    });
6033  };
6034
6035  /**
6036   * Data Attribute API
6037   */
6038  $(function() {
6039    $('[data-tabs]').dacTabs();
6040  });
6041})(jQuery);
6042
6043/* Toast Component */
6044(function($) {
6045  'use strict';
6046  /**
6047   * @constant
6048   * @type {String}
6049   */
6050  var LOCAL_STORAGE_KEY = 'toast-closed-index';
6051
6052  /**
6053   * Dictionary from local storage.
6054   */
6055  var toastDictionary = localStorage.getItem(LOCAL_STORAGE_KEY);
6056  toastDictionary = toastDictionary ? JSON.parse(toastDictionary) : {};
6057
6058  /**
6059   * Variable used for caching the body.
6060   */
6061  var bodyCached;
6062
6063  /**
6064   * @param {HTMLElement} el - The DOM element.
6065   * @param {Object} options
6066   * @constructor
6067   */
6068  function Toast(el, options) {
6069    this.el = $(el);
6070    this.options = $.extend({}, Toast.DEFAULTS_, options);
6071    this.init();
6072  }
6073
6074  Toast.DEFAULTS_ = {
6075    closeBtnClass: 'dac-toast-close-btn',
6076    closeDuration: 200,
6077    visibleClass: 'dac-visible',
6078    wrapClass: 'dac-toast-wrap'
6079  };
6080
6081  /**
6082   * Generate a close button.
6083   * @returns {*|HTMLElement}
6084   */
6085  Toast.prototype.closeBtn = function() {
6086    this.closeBtnEl = this.closeBtnEl || $('<button class="' + this.options.closeBtnClass + '">' +
6087      '<i class="dac-sprite dac-close-black"></i>' +
6088    '</button>');
6089    return this.closeBtnEl;
6090  };
6091
6092  /**
6093   * Initialize a new toast element
6094   */
6095  Toast.prototype.init = function() {
6096    this.hash = this.el.text().replace(/[\s\n\t]/g, '').split('').slice(0, 128).join('');
6097
6098    if (toastDictionary[this.hash]) {
6099      return;
6100    }
6101
6102    this.closeBtn().on('click', this.onClickHandler.bind(this));
6103    this.el.find('.' + this.options.wrapClass).append(this.closeBtn());
6104    this.el.addClass(this.options.visibleClass);
6105    this.dynamicPadding(this.el.outerHeight());
6106  };
6107
6108  /**
6109   * Add padding to make sure all page is visible.
6110   */
6111  Toast.prototype.dynamicPadding = function(val) {
6112    var currentPadding = parseInt(bodyCached.css('padding-bottom') || 0);
6113    bodyCached.css('padding-bottom', val + currentPadding);
6114  };
6115
6116  /**
6117   * Remove a toast from the DOM
6118   */
6119  Toast.prototype.remove = function() {
6120    this.dynamicPadding(-this.el.outerHeight());
6121    this.el.remove();
6122  };
6123
6124  /**
6125   * Handle removal of the toast.
6126   */
6127  Toast.prototype.onClickHandler = function() {
6128    // Only fadeout toasts from top of stack. Others are removed immediately.
6129    var duration = this.el.index() === 0 ? this.options.closeDuration : 0;
6130    this.el.fadeOut(duration, this.remove.bind(this));
6131
6132    // Save closed state.
6133    toastDictionary[this.hash] = 1;
6134    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(toastDictionary));
6135  };
6136
6137  /**
6138   * jQuery plugin
6139   * @param  {object} options - Override default options.
6140   */
6141  $.fn.dacToast = function() {
6142    return this.each(function() {
6143      var el = $(this);
6144      new Toast(el, el.data());
6145    });
6146  };
6147
6148  /**
6149   * Data Attribute API
6150   */
6151  $(function() {
6152    bodyCached = $('#body-content');
6153    $('[data-toast]').dacToast();
6154  });
6155})(jQuery);
6156
6157(function($) {
6158  function Toggle(el) {
6159    $(el).on('click.dac.togglesection', this.toggle);
6160  }
6161
6162  Toggle.prototype.toggle = function() {
6163    var $this = $(this);
6164
6165    var $parent = getParent($this);
6166    var isExpanded = $parent.hasClass('is-expanded');
6167
6168    transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded);
6169    $parent.toggleClass('is-expanded');
6170
6171    return false;
6172  };
6173
6174  function getParent($this) {
6175    var selector = $this.attr('data-target');
6176
6177    if (!selector) {
6178      selector = $this.attr('href');
6179      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
6180    }
6181
6182    var $parent = selector && $(selector);
6183
6184    $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle');
6185
6186    return $parent.length ? $parent : $this.parent();
6187  }
6188
6189  /**
6190   * Runs a transition of max-height along with responsive styles which hide or expand the element.
6191   * @param $el
6192   * @param visible
6193   */
6194  function transitionMaxHeight($el, visible) {
6195    var contentHeight = $el.prop('scrollHeight');
6196    var targetHeight = visible ? contentHeight : 0;
6197    var duration = $el.transitionDuration();
6198
6199    // If we're hiding, first set the maxHeight we're transitioning from.
6200    if (!visible) {
6201      $el.css({
6202          transitionDuration: '0s',
6203          maxHeight: contentHeight + 'px'
6204        })
6205        .resolveStyles()
6206        .css('transitionDuration', '');
6207    }
6208
6209    // Transition to new state
6210    $el.css('maxHeight', targetHeight);
6211
6212    // Reset maxHeight to css value after transition.
6213    setTimeout(function() {
6214      $el.css({
6215          transitionDuration: '0s',
6216          maxHeight: ''
6217        })
6218        .resolveStyles()
6219        .css('transitionDuration', '');
6220    }, duration);
6221  }
6222
6223  // Utility to get the transition duration for the element.
6224  $.fn.transitionDuration = function() {
6225    var d = $(this).css('transitionDuration') || '0s';
6226
6227    return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0);
6228  };
6229
6230  // jQuery plugin
6231  $.fn.toggleSection = function(option) {
6232    return this.each(function() {
6233      var $this = $(this);
6234      var data = $this.data('dac.togglesection');
6235      if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));}
6236      if (typeof option === 'string') {data[option].call($this);}
6237    });
6238  };
6239
6240  // Data api
6241  $(document)
6242    .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle);
6243})(jQuery);
6244
6245(function(window) {
6246  /**
6247   * Media query breakpoints. Should match CSS.
6248   */
6249  var BREAKPOINTS = {
6250    mobile: [0, 719],
6251    tablet: [720, 959],
6252    desktop: [960, 9999]
6253  };
6254
6255  /**
6256   * Fisher-Yates Shuffle (Knuth shuffle).
6257   * @param {Array} input
6258   * @returns {Array} shuffled array.
6259   */
6260  function shuffle(input) {
6261    for (var i = input.length; i >= 0; i--) {
6262      var randomIndex = Math.floor(Math.random() * (i + 1));
6263      var randomItem = input[randomIndex];
6264      input[randomIndex] = input[i];
6265      input[i] = randomItem;
6266    }
6267
6268    return input;
6269  }
6270
6271  /**
6272   * Matches media breakpoints like in CSS.
6273   * @param {string} form of either mobile, tablet or desktop.
6274   */
6275  function matchesMedia(form) {
6276    var breakpoint = BREAKPOINTS[form];
6277    return window.innerWidth >= breakpoint[0] && window.innerWidth <= breakpoint[1];
6278  }
6279
6280  window.util = {
6281    shuffle: shuffle,
6282    matchesMedia: matchesMedia
6283  };
6284})(window);
6285
6286(function($, window) {
6287  'use strict';
6288
6289  var YouTubePlayer = (function() {
6290    var player;
6291
6292    function VideoPlayer() {
6293      this.mPlayerPaused = false;
6294      this.doneSetup = false;
6295    }
6296
6297    VideoPlayer.prototype.setup = function() {
6298      // loads the IFrame Player API code asynchronously.
6299      $.getScript('https://www.youtube.com/iframe_api');
6300
6301      // Add the shadowbox HTML to the body
6302      $('body').prepend(
6303'<div id="video-player" class="Video">' +
6304  '<div id="video-overlay" class="Video-overlay" />' +
6305  '<div class="Video-container">' +
6306    '<div class="Video-frame">' +
6307      '<span class="Video-loading">Loading&hellip;</span>' +
6308      '<div id="youTubePlayer"></div>' +
6309    '</div>' +
6310    '<div class="Video-controls">' +
6311      '<button id="picture-in-picture" class="Video-button Video-button--picture-in-picture">' +
6312      '<button id="close-video" class="Video-button Video-button--close" />' +
6313    '</div>' +
6314  '</div>' +
6315'</div>');
6316
6317      this.videoPlayer = $('#video-player');
6318
6319      var pictureInPictureButton = this.videoPlayer.find('#picture-in-picture');
6320      pictureInPictureButton.on('click.aranja', this.toggleMinimizeVideo.bind(this));
6321
6322      var videoOverlay = this.videoPlayer.find('#video-overlay');
6323      var closeButton = this.videoPlayer.find('#close-video');
6324      var closeVideo = this.closeVideo.bind(this);
6325      videoOverlay.on('click.aranja', closeVideo);
6326      closeButton.on('click.aranja', closeVideo);
6327
6328      this.doneSetup = true;
6329    };
6330
6331    VideoPlayer.prototype.startYouTubePlayer = function(videoId) {
6332      this.videoPlayer.show();
6333
6334      if (!this.isLoaded) {
6335        this.queueVideo = videoId;
6336        return;
6337      }
6338
6339      this.mPlayerPaused = false;
6340      // check if we've already created this player
6341      if (!this.youTubePlayer) {
6342        // check if there's a start time specified
6343        var idAndHash = videoId.split('#');
6344        var startTime = 0;
6345        if (idAndHash.length > 1) {
6346          startTime = idAndHash[1].split('t=')[1] !== undefined ? idAndHash[1].split('t=')[1] : 0;
6347        }
6348        // enable localized player
6349        var lang = getLangPref();
6350        var captionsOn = lang === 'en' ? 0 : 1;
6351
6352        this.youTubePlayer = new YT.Player('youTubePlayer', {
6353          height: 720,
6354          width: 1280,
6355          videoId: idAndHash[0],
6356          // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
6357          playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn},
6358          // jscs:enable
6359          events: {
6360            'onReady': this.onPlayerReady.bind(this),
6361            'onStateChange': this.onPlayerStateChange.bind(this)
6362          }
6363        });
6364      } else {
6365        // if a video different from the one already playing was requested, cue it up
6366        if (videoId !== this.getVideoId()) {
6367          this.youTubePlayer.cueVideoById(videoId);
6368        }
6369        this.youTubePlayer.playVideo();
6370      }
6371    };
6372
6373    VideoPlayer.prototype.onPlayerReady = function(event) {
6374      if (!isMobile) {
6375        event.target.playVideo();
6376        this.mPlayerPaused = false;
6377      }
6378    };
6379
6380    VideoPlayer.prototype.toggleMinimizeVideo = function(event) {
6381      event.stopPropagation();
6382      this.videoPlayer.toggleClass('Video--picture-in-picture');
6383    };
6384
6385    VideoPlayer.prototype.closeVideo = function() {
6386      try {
6387        this.youTubePlayer.pauseVideo();
6388      } catch (e) {
6389      }
6390      this.videoPlayer.fadeOut(200, function() {
6391        this.videoPlayer.removeClass('Video--picture-in-picture');
6392      }.bind(this));
6393    };
6394
6395    VideoPlayer.prototype.getVideoId = function() {
6396      // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
6397      return this.youTubePlayer && this.youTubePlayer.getVideoData().video_id;
6398      // jscs:enable
6399    };
6400
6401    /* Track youtube playback for analytics */
6402    VideoPlayer.prototype.onPlayerStateChange = function(event) {
6403      var videoId = this.getVideoId();
6404      var currentTime = this.youTubePlayer && this.youTubePlayer.getCurrentTime();
6405
6406      // Video starts, send the video ID
6407      if (event.data === YT.PlayerState.PLAYING) {
6408        if (this.mPlayerPaused) {
6409          ga('send', 'event', 'Videos', 'Resume', videoId);
6410        } else {
6411          // track the start playing event so we know from which page the video was selected
6412          ga('send', 'event', 'Videos', 'Start: ' + videoId, 'on: ' + document.location.href);
6413        }
6414        this.mPlayerPaused = false;
6415      }
6416
6417      // Video paused, send video ID and video elapsed time
6418      if (event.data === YT.PlayerState.PAUSED) {
6419        ga('send', 'event', 'Videos', 'Paused', videoId, currentTime);
6420        this.mPlayerPaused = true;
6421      }
6422
6423      // Video finished, send video ID and video elapsed time
6424      if (event.data === YT.PlayerState.ENDED) {
6425        ga('send', 'event', 'Videos', 'Finished', videoId, currentTime);
6426        this.mPlayerPaused = true;
6427      }
6428    };
6429
6430    return {
6431      getPlayer: function() {
6432        if (!player) {
6433          player = new VideoPlayer();
6434        }
6435
6436        return player;
6437      }
6438    };
6439  })();
6440
6441  var videoPlayer = YouTubePlayer.getPlayer();
6442
6443  window.onYouTubeIframeAPIReady = function() {
6444    videoPlayer.isLoaded = true;
6445
6446    if (videoPlayer.queueVideo) {
6447      videoPlayer.startYouTubePlayer(videoPlayer.queueVideo);
6448    }
6449  };
6450
6451  function wrapLinkInPlayer(e) {
6452    e.preventDefault();
6453
6454    if (!videoPlayer.doneSetup) {
6455      videoPlayer.setup();
6456    }
6457
6458    var videoIdMatches = $(e.currentTarget).attr('href').match(/(?:youtu.be\/|v=)([^&]*)/);
6459    var videoId = videoIdMatches && videoIdMatches[1];
6460
6461    if (videoId) {
6462      videoPlayer.startYouTubePlayer(videoId);
6463    }
6464  }
6465
6466  $(document).on('click.video', 'a[href*="youtube.com/watch"], a[href*="youtu.be"]', wrapLinkInPlayer);
6467})(jQuery, window);
6468
6469/**
6470 * Wide table
6471 *
6472 * Wraps tables in a scrollable area so you can read them on mobile.
6473 */
6474(function($) {
6475  function initWideTable() {
6476    $('table.jd-sumtable').each(function(i, table) {
6477      $(table).wrap('<div class="dac-expand wide-table">');
6478    });
6479  }
6480
6481  $(function() {
6482    initWideTable();
6483  });
6484})(jQuery);
6485