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