docs.js revision 056478d28b535cb6a7f48aacb679caae0d62c18f
1var classesNav;
2var devdocNav;
3var sidenav;
4var cookie_namespace = 'android_developer';
5var NAV_PREF_TREE = "tree";
6var NAV_PREF_PANELS = "panels";
7var nav_pref;
8var isMobile = false; // true if mobile, so we can adjust some layout
9var mPagePath; // initialized in ready() function
10
11var basePath = getBaseUri(location.pathname);
12var SITE_ROOT = toRoot + basePath.substring(1,basePath.indexOf("/",1));
13var GOOGLE_DATA; // combined data for google service apis, used for search suggest
14
15// Ensure that all ajax getScript() requests allow caching
16$.ajaxSetup({
17  cache: true
18});
19
20/******  ON LOAD SET UP STUFF *********/
21
22$(document).ready(function() {
23
24  // show lang dialog if the URL includes /intl/
25  //if (location.pathname.substring(0,6) == "/intl/") {
26  //  var lang = location.pathname.split('/')[2];
27   // if (lang != getLangPref()) {
28   //   $("#langMessage a.yes").attr("onclick","changeLangPref('" + lang
29   //       + "', true); $('#langMessage').hide(); return false;");
30  //    $("#langMessage .lang." + lang).show();
31   //   $("#langMessage").show();
32   // }
33  //}
34
35  // load json file for JD doc search suggestions
36  $.getScript(toRoot + 'jd_lists_unified.js');
37  // load json file for Android API search suggestions
38  $.getScript(toRoot + 'reference/lists.js');
39  // load json files for Google services API suggestions
40  $.getScript(toRoot + 'reference/gcm_lists.js', function(data, textStatus, jqxhr) {
41      // once the GCM json (GCM_DATA) is loaded, load the GMS json (GMS_DATA) and merge the data
42      if(jqxhr.status === 200) {
43          $.getScript(toRoot + 'reference/gms_lists.js', function(data, textStatus, jqxhr) {
44              if(jqxhr.status === 200) {
45                  // combine GCM and GMS data
46                  GOOGLE_DATA = GMS_DATA;
47                  var start = GOOGLE_DATA.length;
48                  for (var i=0; i<GCM_DATA.length; i++) {
49                      GOOGLE_DATA.push({id:start+i, label:GCM_DATA[i].label,
50                              link:GCM_DATA[i].link, type:GCM_DATA[i].type});
51                  }
52              }
53          });
54      }
55  });
56
57  // setup keyboard listener for search shortcut
58  $('body').keyup(function(event) {
59    if (event.which == 191 && $(event.target).is(':not(:input)')) {
60      $('#search_autocomplete').focus();
61    }
62  });
63
64  // init the fullscreen toggle click event
65  $('#nav-swap .fullscreen').click(function(){
66    if ($(this).hasClass('disabled')) {
67      toggleFullscreen(true);
68    } else {
69      toggleFullscreen(false);
70    }
71  });
72
73  // initialize the divs with custom scrollbars
74  if (window.innerWidth >= 720) {
75    $('.scroll-pane').jScrollPane({verticalGutter: 0});
76  }
77
78  // set up the search close button
79  $('#search-close').on('click touchend', function() {
80    $searchInput = $('#search_autocomplete');
81    $searchInput.attr('value', '');
82    $(this).addClass("hide");
83    $("#search-container").removeClass('active');
84    $("#search_autocomplete").blur();
85    search_focus_changed($searchInput.get(), false);
86    hideResults();
87  });
88
89
90  //Set up search
91  $("#search_autocomplete").focus(function() {
92    $("#search-container").addClass('active');
93  })
94  $("#search-container").on('mouseover touchend', function(e) {
95    if ($(e.target).is('#search-close')) { return; }
96    $("#search-container").addClass('active');
97    $("#search_autocomplete").focus();
98  })
99  $("#search-container").mouseout(function() {
100    if ($("#search_autocomplete").is(":focus")) return;
101    if ($("#search_autocomplete").val() == '') {
102      setTimeout(function(){
103        $("#search-container").removeClass('active');
104        $("#search_autocomplete").blur();
105      },250);
106    }
107  })
108  $("#search_autocomplete").blur(function() {
109    if ($("#search_autocomplete").val() == '') {
110      $("#search-container").removeClass('active');
111    }
112  })
113
114
115  // prep nav expandos
116  var pagePath = document.location.pathname;
117  // account for intl docs by removing the intl/*/ path
118  if (pagePath.indexOf("/intl/") == 0) {
119    pagePath = pagePath.substr(pagePath.indexOf("/",6)); // start after intl/ to get last /
120  }
121
122  if (pagePath.indexOf(SITE_ROOT) == 0) {
123    if (pagePath == '' || pagePath.charAt(pagePath.length - 1) == '/') {
124      pagePath += 'index.html';
125    }
126  }
127
128  // Need a copy of the pagePath before it gets changed in the next block;
129  // it's needed to perform proper tab highlighting in offline docs (see rootDir below)
130  var pagePathOriginal = pagePath;
131  if (SITE_ROOT.match(/\.\.\//) || SITE_ROOT == '') {
132    // If running locally, SITE_ROOT will be a relative path, so account for that by
133    // finding the relative URL to this page. This will allow us to find links on the page
134    // leading back to this page.
135    var pathParts = pagePath.split('/');
136    var relativePagePathParts = [];
137    var upDirs = (SITE_ROOT.match(/(\.\.\/)+/) || [''])[0].length / 3;
138    for (var i = 0; i < upDirs; i++) {
139      relativePagePathParts.push('..');
140    }
141    for (var i = 0; i < upDirs; i++) {
142      relativePagePathParts.push(pathParts[pathParts.length - (upDirs - i) - 1]);
143    }
144    relativePagePathParts.push(pathParts[pathParts.length - 1]);
145    pagePath = relativePagePathParts.join('/');
146  } else {
147    // Otherwise the page path is already an absolute URL
148  }
149
150  // Highlight the header tabs...
151  // highlight Design tab
152  var urlSegments = pagePathOriginal.split('/');
153  var navEl = $(".dac-nav-list");
154  var subNavEl = navEl.find(".dac-nav-secondary");
155  var parentNavEl;
156
157  if ($("body").hasClass("design")) {
158    navEl.find("> li.design > a").addClass("selected");
159  // highlight About tabs
160  } else if ($("body").hasClass("about")) {
161    if (urlSegments[1] == "about" || urlSegments[1] == "wear" || urlSegments[1] == "tv" || urlSegments[1] == "auto") {
162      navEl.find("> li.home > a").addClass('has-subnav');
163      subNavEl.find("li." + urlSegments[1] + " > a").addClass("selected");
164    } else {
165      navEl.find("> li.home > a").addClass('selected');
166    }
167
168// highlight NDK tabs
169  } else if ($("body").hasClass("ndk")) {
170    parentNavEl = navEl.find("> li.ndk > a");
171    parentNavEl.addClass('has-subnav');
172    if ($("body").hasClass("guide")) {
173      navEl.find("> li.guides > a").addClass("selected ndk");
174    } else if ($("body").hasClass("reference")) {
175      navEl.find("> li.reference > a").addClass("selected ndk");
176    } else if ($("body").hasClass("samples")) {
177      navEl.find("> li.samples > a").addClass("selected ndk");
178    } else if ($("body").hasClass("downloads")) {
179      navEl.find("> li.downloads > a").addClass("selected ndk");
180    }
181
182  // highlight Develop tab
183  } else if ($("body").hasClass("develop") || $("body").hasClass("google")) {
184    parentNavEl = navEl.find("> li.develop > a");
185    parentNavEl.addClass('has-subnav');
186
187    // In Develop docs, also highlight appropriate sub-tab
188    if (urlSegments[1] == "training") {
189      subNavEl.find("li.training > a").addClass("selected");
190    } else if (urlSegments[1] == "guide") {
191      subNavEl.find("li.guide > a").addClass("selected");
192    } else if (urlSegments[1] == "reference") {
193      // If the root is reference, but page is also part of Google Services, select Google
194      if ($("body").hasClass("google")) {
195        subNavEl.find("li.google > a").addClass("selected");
196      } else {
197        subNavEl.find("li.reference > a").addClass("selected");
198      }
199    } else if ((urlSegments[1] == "tools") || (urlSegments[1] == "sdk")) {
200      subNavEl.find("li.tools > a").addClass("selected");
201    } else if ($("body").hasClass("google")) {
202      subNavEl.find("li.google > a").addClass("selected");
203    } else if ($("body").hasClass("samples")) {
204      subNavEl.find("li.samples > a").addClass("selected");
205    } else if ($("body").hasClass("preview")) {
206      subNavEl.find("li.preview > a").addClass("selected");
207    } else {
208      parentNavEl.removeClass('has-subnav').addClass("selected");
209    }
210  // highlight Distribute tab
211  } else if ($("body").hasClass("distribute")) {
212    parentNavEl = navEl.find("> li.distribute > a");
213    parentNavEl.addClass('has-subnav');
214
215    if (urlSegments[2] == "users") {
216      subNavEl.find("li.users > a").addClass("selected");
217    } else if (urlSegments[2] == "engage") {
218      subNavEl.find("li.engage > a").addClass("selected");
219    } else if (urlSegments[2] == "monetize") {
220      subNavEl.find("li.monetize > a").addClass("selected");
221    } else if (urlSegments[2] == "analyze") {
222      subNavEl.find("li.analyze > a").addClass("selected");
223    } else if (urlSegments[2] == "tools") {
224      subNavEl.find("li.essentials > a").addClass("selected");
225    } else if (urlSegments[2] == "stories") {
226      subNavEl.find("li.stories > a").addClass("selected");
227    } else if (urlSegments[2] == "essentials") {
228      subNavEl.find("li.essentials > a").addClass("selected");
229    } else if (urlSegments[2] == "googleplay") {
230      subNavEl.find("li.googleplay > a").addClass("selected");
231    } else {
232      parentNavEl.removeClass('has-subnav').addClass("selected");
233    }
234  }
235
236  // set global variable so we can highlight the sidenav a bit later (such as for google reference)
237  // and highlight the sidenav
238  mPagePath = pagePath;
239  highlightSidenav();
240  buildBreadcrumbs();
241
242  // set up prev/next links if they exist
243  var $selNavLink = $('#nav').find('a[href="' + pagePath + '"]');
244  var $selListItem;
245  if ($selNavLink.length) {
246    $selListItem = $selNavLink.closest('li');
247
248    // set up prev links
249    var $prevLink = [];
250    var $prevListItem = $selListItem.prev('li');
251
252    var crossBoundaries = ($("body.design").length > 0) || ($("body.guide").length > 0) ? true :
253false; // navigate across topic boundaries only in design docs
254    if ($prevListItem.length) {
255      if ($prevListItem.hasClass('nav-section') || crossBoundaries) {
256        // jump to last topic of previous section
257        $prevLink = $prevListItem.find('a:last');
258      } else if (!$selListItem.hasClass('nav-section')) {
259        // jump to previous topic in this section
260        $prevLink = $prevListItem.find('a:eq(0)');
261      }
262    } else {
263      // jump to this section's index page (if it exists)
264      var $parentListItem = $selListItem.parents('li');
265      $prevLink = $selListItem.parents('li').find('a');
266
267      // except if cross boundaries aren't allowed, and we're at the top of a section already
268      // (and there's another parent)
269      if (!crossBoundaries && $parentListItem.hasClass('nav-section')
270                           && $selListItem.hasClass('nav-section')) {
271        $prevLink = [];
272      }
273    }
274
275    // set up next links
276    var $nextLink = [];
277    var startClass = false;
278    var isCrossingBoundary = false;
279
280    if ($selListItem.hasClass('nav-section') && $selListItem.children('div.empty').length == 0) {
281      // we're on an index page, jump to the first topic
282      $nextLink = $selListItem.find('ul:eq(0)').find('a:eq(0)');
283
284      // if there aren't any children, go to the next section (required for About pages)
285      if($nextLink.length == 0) {
286        $nextLink = $selListItem.next('li').find('a');
287      } else if ($('.topic-start-link').length) {
288        // as long as there's a child link and there is a "topic start link" (we're on a landing)
289        // then set the landing page "start link" text to be the first doc title
290        $('.topic-start-link').text($nextLink.text().toUpperCase());
291      }
292
293      // If the selected page has a description, then it's a class or article homepage
294      if ($selListItem.find('a[description]').length) {
295        // this means we're on a class landing page
296        startClass = true;
297      }
298    } else {
299      // jump to the next topic in this section (if it exists)
300      $nextLink = $selListItem.next('li').find('a:eq(0)');
301      if ($nextLink.length == 0) {
302        isCrossingBoundary = true;
303        // no more topics in this section, jump to the first topic in the next section
304        $nextLink = $selListItem.parents('li:eq(0)').next('li').find('a:eq(0)');
305        if (!$nextLink.length) {  // Go up another layer to look for next page (lesson > class > course)
306          $nextLink = $selListItem.parents('li:eq(1)').next('li.nav-section').find('a:eq(0)');
307          if ($nextLink.length == 0) {
308            // if that doesn't work, we're at the end of the list, so disable NEXT link
309            $('.next-page-link').attr('href','').addClass("disabled")
310                                .click(function() { return false; });
311            // and completely hide the one in the footer
312            $('.content-footer .next-page-link').hide();
313          }
314        }
315      }
316    }
317
318    if (startClass) {
319      $('.start-class-link').attr('href', $nextLink.attr('href')).removeClass("hide");
320
321      // if there's no training bar (below the start button),
322      // then we need to add a bottom border to button
323      if (!$("#tb").length) {
324        $('.start-class-link').css({'border-bottom':'1px solid #DADADA'});
325      }
326    } else if (isCrossingBoundary && !$('body.design').length) {  // Design always crosses boundaries
327      $('.content-footer.next-class').show();
328      $('.next-page-link').attr('href','')
329                          .removeClass("hide").addClass("disabled")
330                          .click(function() { return false; });
331      // and completely hide the one in the footer
332      $('.content-footer .next-page-link').hide();
333      if ($nextLink.length) {
334        $('.next-class-link').attr('href',$nextLink.attr('href'))
335                             .removeClass("hide")
336                             .append(": " + $nextLink.html());
337        $('.next-class-link').find('.new').empty();
338      }
339    } else {
340      $('.next-page-link').attr('href', $nextLink.attr('href'))
341                          .removeClass("hide");
342      // for the footer link, also add the next page title
343      $('.content-footer .next-page-link').append(": " + $nextLink.html());
344    }
345
346    if (!startClass && $prevLink.length) {
347      var prevHref = $prevLink.attr('href');
348      if (prevHref == SITE_ROOT + 'index.html') {
349        // Don't show Previous when it leads to the homepage
350      } else {
351        $('.prev-page-link').attr('href', $prevLink.attr('href')).removeClass("hide");
352      }
353    }
354
355  }
356
357
358
359  // Set up the course landing pages for Training with class names and descriptions
360  if ($('body.trainingcourse').length) {
361    var $classLinks = $selListItem.find('ul li a').not('#nav .nav-section .nav-section ul a');
362
363    // create an array for all the class descriptions
364    var $classDescriptions = new Array($classLinks.length);
365    var lang = getLangPref();
366    $classLinks.each(function(index) {
367      var langDescr = $(this).attr(lang + "-description");
368      if (typeof langDescr !== 'undefined' && langDescr !== false) {
369        // if there's a class description in the selected language, use that
370        $classDescriptions[index] = langDescr;
371      } else {
372        // otherwise, use the default english description
373        $classDescriptions[index] = $(this).attr("description");
374      }
375    });
376
377    var $olClasses  = $('<ol class="class-list"></ol>');
378    var $liClass;
379    var $h2Title;
380    var $pSummary;
381    var $olLessons;
382    var $liLesson;
383    $classLinks.each(function(index) {
384      $liClass  = $('<li class="clearfix"></li>');
385      $h2Title  = $('<a class="title" href="'+$(this).attr('href')+'"><h2 class="norule">' + $(this).html()+'</h2><span></span></a>');
386      $pSummary = $('<p class="description">' + $classDescriptions[index] + '</p>');
387
388      $olLessons  = $('<ol class="lesson-list"></ol>');
389
390      $lessons = $(this).closest('li').find('ul li a');
391
392      if ($lessons.length) {
393        $lessons.each(function(index) {
394          $olLessons.append('<li><a href="'+$(this).attr('href')+'">' + $(this).html()+'</a></li>');
395        });
396      } else {
397        $pSummary.addClass('article');
398      }
399
400      $liClass.append($h2Title).append($pSummary).append($olLessons);
401      $olClasses.append($liClass);
402    });
403    $('.jd-descr').append($olClasses);
404  }
405
406  // Set up expand/collapse behavior
407  initExpandableNavItems("#nav");
408
409
410  $(".scroll-pane").scroll(function(event) {
411      event.preventDefault();
412      return false;
413  });
414
415  /* Resize nav height when window height changes */
416  $(window).resize(function() {
417    if ($('#side-nav').length == 0) return;
418    setNavBarDimensions(); // do this even if sidenav isn't fixed because it could become fixed
419    // make sidenav behave when resizing the window and side-scolling is a concern
420    updateSideNavDimensions();
421    checkSticky();
422    resizeNav(250);
423  });
424
425  if ($('#devdoc-nav').length) {
426    setNavBarDimensions();
427  }
428
429
430  // Set up play-on-hover <video> tags.
431  $('video.play-on-hover').bind('click', function(){
432    $(this).get(0).load(); // in case the video isn't seekable
433    $(this).get(0).play();
434  });
435
436  // Set up tooltips
437  var TOOLTIP_MARGIN = 10;
438  $('acronym,.tooltip-link').each(function() {
439    var $target = $(this);
440    var $tooltip = $('<div>')
441        .addClass('tooltip-box')
442        .append($target.attr('title'))
443        .hide()
444        .appendTo('body');
445    $target.removeAttr('title');
446
447    $target.hover(function() {
448      // in
449      var targetRect = $target.offset();
450      targetRect.width = $target.width();
451      targetRect.height = $target.height();
452
453      $tooltip.css({
454        left: targetRect.left,
455        top: targetRect.top + targetRect.height + TOOLTIP_MARGIN
456      });
457      $tooltip.addClass('below');
458      $tooltip.show();
459    }, function() {
460      // out
461      $tooltip.hide();
462    });
463  });
464
465  // Set up <h2> deeplinks
466  $('h2').click(function() {
467    var id = $(this).attr('id');
468    if (id) {
469      if (history && history.replaceState) {
470        // Change url without scrolling.
471        history.replaceState({}, '', '#' + id);
472      } else {
473        document.location.hash = id;
474      }
475    }
476  });
477
478  //Loads the +1 button
479  var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
480  po.src = 'https://apis.google.com/js/plusone.js';
481  var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
482
483  $(".scroll-pane").removeAttr("tabindex"); // get rid of tabindex added by jscroller
484
485  if ($(".scroll-pane").length > 1) {
486    // Check if there's a user preference for the panel heights
487    var cookieHeight = readCookie("reference_height");
488    if (cookieHeight) {
489      restoreHeight(cookieHeight);
490    }
491  }
492
493  // Resize once loading is finished
494  resizeNav();
495  // Check if there's an anchor that we need to scroll into view.
496  // A delay is needed, because some browsers do not immediately scroll down to the anchor
497  window.setTimeout(offsetScrollForSticky, 100);
498
499  /* init the language selector based on user cookie for lang */
500  loadLangPref();
501  changeNavLang(getLangPref());
502
503  /* setup event handlers to ensure the overflow menu is visible while picking lang */
504  $("#language select")
505      .mousedown(function() {
506        $("div.morehover").addClass("hover"); })
507      .blur(function() {
508        $("div.morehover").removeClass("hover"); });
509
510  /* some global variable setup */
511  resizePackagesNav = $("#resize-packages-nav");
512  classesNav = $("#classes-nav");
513  devdocNav = $("#devdoc-nav");
514
515  var cookiePath = "";
516  if (location.href.indexOf("/reference/") != -1) {
517    cookiePath = "reference_";
518  } else if (location.href.indexOf("/guide/") != -1) {
519    cookiePath = "guide_";
520  } else if (location.href.indexOf("/tools/") != -1) {
521    cookiePath = "tools_";
522  } else if (location.href.indexOf("/training/") != -1) {
523    cookiePath = "training_";
524  } else if (location.href.indexOf("/design/") != -1) {
525    cookiePath = "design_";
526  } else if (location.href.indexOf("/distribute/") != -1) {
527    cookiePath = "distribute_";
528  }
529
530
531  /* setup shadowbox for any videos that want it */
532  var $videoLinks = $("a.video-shadowbox-button, a.notice-developers-video");
533  if ($videoLinks.length) {
534    // if there's at least one, add the shadowbox HTML to the body
535    $('body').prepend(
536'<div id="video-container">'+
537  '<div id="video-frame">'+
538    '<div class="video-close">'+
539      '<span id="icon-video-close" onclick="closeVideo()">&nbsp;</span>'+
540    '</div>'+
541    '<div id="youTubePlayer"></div>'+
542  '</div>'+
543'</div>');
544
545    // loads the IFrame Player API code asynchronously.
546    $.getScript("https://www.youtube.com/iframe_api");
547
548    $videoLinks.each(function() {
549      var videoId = $(this).attr('href').split('?v=')[1];
550      $(this).click(function(event) {
551        event.preventDefault();
552        startYouTubePlayer(videoId);
553      });
554    });
555  }
556});
557// END of the onload event
558
559
560var youTubePlayer;
561function onYouTubeIframeAPIReady() {
562}
563
564/* Returns the height the shadowbox video should be. It's based on the current
565   height of the "video-frame" element, which is 100% height for the window.
566   Then minus the margin so the video isn't actually the full window height. */
567function getVideoHeight() {
568  var frameHeight = $("#video-frame").height();
569  var marginTop = $("#video-frame").css('margin-top').split('px')[0];
570  return frameHeight - (marginTop * 2);
571}
572
573var mPlayerPaused = false;
574
575function startYouTubePlayer(videoId) {
576  $("#video-container").show();
577  $("#video-frame").show();
578  mPlayerPaused = false;
579
580  // compute the size of the player so it's centered in window
581  var maxWidth = 940;  // the width of the web site content
582  var videoAspect = .5625; // based on 1280x720 resolution
583  var maxHeight = maxWidth * videoAspect;
584  var videoHeight = getVideoHeight();
585  var videoWidth = videoHeight / videoAspect;
586  if (videoWidth > maxWidth) {
587    videoWidth = maxWidth;
588    videoHeight = maxHeight;
589  }
590  $("#video-frame").css('width', videoWidth);
591
592  // check if we've already created this player
593  if (youTubePlayer == null) {
594    // check if there's a start time specified
595    var idAndHash = videoId.split("#");
596    var startTime = 0;
597    if (idAndHash.length > 1) {
598      startTime = idAndHash[1].split("t=")[1] != undefined ? idAndHash[1].split("t=")[1] : 0;
599    }
600    // enable localized player
601    var lang = getLangPref();
602    var captionsOn = lang == 'en' ? 0 : 1;
603
604    youTubePlayer = new YT.Player('youTubePlayer', {
605      height: videoHeight,
606      width: videoWidth,
607      videoId: idAndHash[0],
608      playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn},
609      events: {
610        'onReady': onPlayerReady,
611        'onStateChange': onPlayerStateChange
612      }
613    });
614  } else {
615    // reset the size in case the user adjusted the window since last play
616    youTubePlayer.setSize(videoWidth, videoHeight);
617    // if a video different from the one already playing was requested, cue it up
618    if (videoId != youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0]) {
619      youTubePlayer.cueVideoById(videoId);
620    }
621    youTubePlayer.playVideo();
622  }
623}
624
625function onPlayerReady(event) {
626  event.target.playVideo();
627  mPlayerPaused = false;
628}
629
630function closeVideo() {
631  try {
632    youTubePlayer.pauseVideo();
633  } catch(e) {
634  }
635  $("#video-container").fadeOut(200);
636}
637
638/* Track youtube playback for analytics */
639function onPlayerStateChange(event) {
640    // Video starts, send the video ID
641    if (event.data == YT.PlayerState.PLAYING) {
642      if (mPlayerPaused) {
643        ga('send', 'event', 'Videos', 'Resume',
644            youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0]);
645      } else {
646        // track the start playing event so we know from which page the video was selected
647        ga('send', 'event', 'Videos', 'Start: ' +
648            youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0],
649            'on: ' + document.location.href);
650      }
651      mPlayerPaused = false;
652    }
653    // Video paused, send video ID and video elapsed time
654    if (event.data == YT.PlayerState.PAUSED) {
655      ga('send', 'event', 'Videos', 'Paused',
656            youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0],
657            youTubePlayer.getCurrentTime());
658      mPlayerPaused = true;
659    }
660    // Video finished, send video ID and video elapsed time
661    if (event.data == YT.PlayerState.ENDED) {
662      ga('send', 'event', 'Videos', 'Finished',
663            youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0],
664            youTubePlayer.getCurrentTime());
665      mPlayerPaused = true;
666    }
667}
668
669
670
671function initExpandableNavItems(rootTag) {
672  $(rootTag + ' li.nav-section .nav-section-header').click(function() {
673    var section = $(this).closest('li.nav-section');
674    if (section.hasClass('expanded')) {
675    /* hide me and descendants */
676      section.find('ul').slideUp(250, function() {
677        // remove 'expanded' class from my section and any children
678        section.closest('li').removeClass('expanded');
679        $('li.nav-section', section).removeClass('expanded');
680        resizeNav();
681      });
682    } else {
683    /* show me */
684      // first hide all other siblings
685      var $others = $('li.nav-section.expanded', $(this).closest('ul')).not('.sticky');
686      $others.removeClass('expanded').children('ul').slideUp(250);
687
688      // now expand me
689      section.closest('li').addClass('expanded');
690      section.children('ul').slideDown(250, function() {
691        resizeNav();
692      });
693    }
694  });
695
696  // Stop expand/collapse behavior when clicking on nav section links
697  // (since we're navigating away from the page)
698  // This selector captures the first instance of <a>, but not those with "#" as the href.
699  $('.nav-section-header').find('a:eq(0)').not('a[href="#"]').click(function(evt) {
700    window.location.href = $(this).attr('href');
701    return false;
702  });
703}
704
705
706/** Create the list of breadcrumb links in the sticky header */
707function buildBreadcrumbs() {
708  var $breadcrumbUl =  $(".dac-header-crumbs");
709  var primaryNavLink = ".dac-nav-list > .dac-nav-item > .dac-nav-link";
710
711  // Add the secondary horizontal nav item, if provided
712  var $selectedSecondNav = $(".dac-nav-secondary .dac-nav-link.selected").clone()
713    .attr('class', 'dac-header-crumbs-link');
714
715  if ($selectedSecondNav.length) {
716    $breadcrumbUl.prepend($('<li class="dac-header-crumbs-item">').append($selectedSecondNav));
717  }
718
719  // Add the primary horizontal nav
720  var $selectedFirstNav = $(primaryNavLink + ".selected, " + primaryNavLink + ".has-subnav").clone()
721    .attr('class', 'dac-header-crumbs-link');
722
723  // If there's no header nav item, use the logo link and title from alt text
724  if ($selectedFirstNav.length < 1) {
725    $selectedFirstNav = $('<a class="dac-header-crumbs-link">')
726        .attr('href', $("div#header .logo a").attr('href'))
727        .text($("div#header .logo img").attr('alt'));
728  }
729  $breadcrumbUl.prepend($('<li class="dac-header-crumbs-item">').append($selectedFirstNav));
730}
731
732
733
734/** Highlight the current page in sidenav, expanding children as appropriate */
735function highlightSidenav() {
736  // if something is already highlighted, undo it. This is for dynamic navigation (Samples index)
737  if ($("ul#nav li.selected").length) {
738    unHighlightSidenav();
739  }
740  // look for URL in sidenav, including the hash
741  var $selNavLink = $('#nav').find('a[href="' + mPagePath + location.hash + '"]');
742
743  // If the selNavLink is still empty, look for it without the hash
744  if ($selNavLink.length == 0) {
745    $selNavLink = $('#nav').find('a[href="' + mPagePath + '"]');
746  }
747
748  var $selListItem;
749  if ($selNavLink.length) {
750    // Find this page's <li> in sidenav and set selected
751    $selListItem = $selNavLink.closest('li');
752    $selListItem.addClass('selected');
753
754    // Traverse up the tree and expand all parent nav-sections
755    $selNavLink.parents('li.nav-section').each(function() {
756      $(this).addClass('expanded');
757      $(this).children('ul').show();
758    });
759  }
760}
761
762function unHighlightSidenav() {
763  $("ul#nav li.selected").removeClass("selected");
764  $('ul#nav li.nav-section.expanded').removeClass('expanded').children('ul').hide();
765}
766
767function toggleFullscreen(enable) {
768  var delay = 20;
769  var enabled = true;
770  var stylesheet = $('link[rel="stylesheet"][class="fullscreen"]');
771  if (enable) {
772    // Currently NOT USING fullscreen; enable fullscreen
773    stylesheet.removeAttr('disabled');
774    $('#nav-swap .fullscreen').removeClass('disabled');
775    $('#devdoc-nav').css({left:''});
776    setTimeout(updateSidenavFullscreenWidth,delay); // need to wait a moment for css to switch
777    enabled = true;
778  } else {
779    // Currently USING fullscreen; disable fullscreen
780    stylesheet.attr('disabled', 'disabled');
781    $('#nav-swap .fullscreen').addClass('disabled');
782    setTimeout(updateSidenavFixedWidth,delay); // need to wait a moment for css to switch
783    enabled = false;
784  }
785  writeCookie("fullscreen", enabled, null);
786  setNavBarDimensions();
787  resizeNav(delay);
788  updateSideNavDimensions();
789  setTimeout(initSidenavHeightResize,delay);
790}
791
792// TODO: Refactor into a closure.
793var navBarLeftPos;
794var navBarWidth;
795function setNavBarDimensions() {
796  navBarLeftPos = $('#body-content').offset().left;
797  navBarWidth = $('#side-nav').width();
798}
799
800
801function updateSideNavDimensions() {
802  var newLeft = $(window).scrollLeft() - navBarLeftPos;
803  $('#devdoc-nav').css({left: -newLeft, width: navBarWidth});
804  $('#devdoc-nav .totop').css({left: -(newLeft - parseInt($('#side-nav').css('padding-left')))});
805}
806
807// TODO: use $(document).ready instead
808function addLoadEvent(newfun) {
809  var current = window.onload;
810  if (typeof window.onload != 'function') {
811    window.onload = newfun;
812  } else {
813    window.onload = function() {
814      current();
815      newfun();
816    }
817  }
818}
819
820var agent = navigator['userAgent'].toLowerCase();
821// If a mobile phone, set flag and do mobile setup
822if ((agent.indexOf("mobile") != -1) ||      // android, iphone, ipod
823    (agent.indexOf("blackberry") != -1) ||
824    (agent.indexOf("webos") != -1) ||
825    (agent.indexOf("mini") != -1)) {        // opera mini browsers
826  isMobile = true;
827}
828
829
830$(document).ready(function() {
831  $("pre:not(.no-pretty-print)").addClass("prettyprint");
832  prettyPrint();
833});
834
835
836
837
838/* ######### RESIZE THE SIDENAV ########## */
839
840function resizeNav(delay) {
841  var $nav = $("#devdoc-nav");
842  var $window = $(window);
843  var navHeight;
844
845  // Get the height of entire window and the total header height.
846  // Then figure out based on scroll position whether the header is visible
847  var windowHeight = $window.height();
848  var scrollTop = $window.scrollTop();
849  var headerHeight = $('#header-wrapper').outerHeight();
850  var headerVisible = scrollTop < stickyTop;
851
852  // get the height of space between nav and top of window.
853  // Could be either margin or top position, depending on whether the nav is fixed.
854  var topMargin = (parseInt($nav.css('top')) || 20) + 1;
855  // add 1 for the #side-nav bottom margin
856
857  // Depending on whether the header is visible, set the side nav's height.
858  if (headerVisible) {
859    // The sidenav height grows as the header goes off screen
860    navHeight = windowHeight - (headerHeight - scrollTop) - topMargin;
861  } else {
862    // Once header is off screen, the nav height is almost full window height
863    navHeight = windowHeight - topMargin;
864  }
865
866
867
868  $scrollPanes = $(".scroll-pane");
869  if ($window.width() < 720) {
870    $nav.css('height', '');
871  } else if ($scrollPanes.length > 1) {
872    // subtract the height of the api level widget and nav swapper from the available nav height
873    navHeight -= ($('#api-nav-header').outerHeight(true) + $('#nav-swap').outerHeight(true));
874
875    $("#swapper").css({height:navHeight + "px"});
876    if ($("#nav-tree").is(":visible")) {
877      $("#nav-tree").css({height:navHeight});
878    }
879
880    var classesHeight = navHeight - parseInt($("#resize-packages-nav").css("height")) - 10 + "px";
881    //subtract 10px to account for drag bar
882
883    // if the window becomes small enough to make the class panel height 0,
884    // then the package panel should begin to shrink
885    if (parseInt(classesHeight) <= 0) {
886      $("#resize-packages-nav").css({height:navHeight - 10}); //subtract 10px for drag bar
887      $("#packages-nav").css({height:navHeight - 10});
888    }
889
890    $("#classes-nav").css({'height':classesHeight, 'margin-top':'10px'});
891    $("#classes-nav .jspContainer").css({height:classesHeight});
892
893
894  } else {
895    $nav.height(navHeight);
896  }
897
898  if (delay) {
899    updateFromResize = true;
900    delayedReInitScrollbars(delay);
901  } else {
902    reInitScrollbars();
903  }
904
905}
906
907var updateScrollbars = false;
908var updateFromResize = false;
909
910/* Re-initialize the scrollbars to account for changed nav size.
911 * This method postpones the actual update by a 1/4 second in order to optimize the
912 * scroll performance while the header is still visible, because re-initializing the
913 * scroll panes is an intensive process.
914 */
915function delayedReInitScrollbars(delay) {
916  // If we're scheduled for an update, but have received another resize request
917  // before the scheduled resize has occured, just ignore the new request
918  // (and wait for the scheduled one).
919  if (updateScrollbars && updateFromResize) {
920    updateFromResize = false;
921    return;
922  }
923
924  // We're scheduled for an update and the update request came from this method's setTimeout
925  if (updateScrollbars && !updateFromResize) {
926    reInitScrollbars();
927    updateScrollbars = false;
928  } else {
929    updateScrollbars = true;
930    updateFromResize = false;
931    setTimeout('delayedReInitScrollbars()',delay);
932  }
933}
934
935/* Re-initialize the scrollbars to account for changed nav size. */
936function reInitScrollbars() {
937  var pane = $(".scroll-pane").each(function(){
938    var api = $(this).data('jsp');
939    if (!api) {return;}
940    api.reinitialise( {verticalGutter:0} );
941  });
942  $(".scroll-pane").removeAttr("tabindex"); // get rid of tabindex added by jscroller
943}
944
945
946/* Resize the height of the nav panels in the reference,
947 * and save the new size to a cookie */
948function saveNavPanels() {
949  var basePath = getBaseUri(location.pathname);
950  var section = basePath.substring(1,basePath.indexOf("/",1));
951  writeCookie("height", resizePackagesNav.css("height"), section);
952}
953
954
955
956function restoreHeight(packageHeight) {
957    $("#resize-packages-nav").height(packageHeight);
958    $("#packages-nav").height(packageHeight);
959  //  var classesHeight = navHeight - packageHeight;
960 //   $("#classes-nav").css({height:classesHeight});
961  //  $("#classes-nav .jspContainer").css({height:classesHeight});
962}
963
964
965
966/* ######### END RESIZE THE SIDENAV HEIGHT ########## */
967
968
969
970
971
972/** Scroll the jScrollPane to make the currently selected item visible
973    This is called when the page finished loading. */
974function scrollIntoView(nav) {
975  return;
976  var $nav = $("#"+nav);
977  var element = $nav.jScrollPane({/* ...settings... */});
978  var api = element.data('jsp');
979
980  if ($nav.is(':visible')) {
981    var $selected = $(".selected", $nav);
982    if ($selected.length == 0) {
983      // If no selected item found, exit
984      return;
985    }
986    // get the selected item's offset from its container nav by measuring the item's offset
987    // relative to the document then subtract the container nav's offset relative to the document
988    var selectedOffset = $selected.offset().top - $nav.offset().top;
989    if (selectedOffset > $nav.height() * .8) { // multiply nav height by .8 so we move up the item
990                                               // if it's more than 80% down the nav
991      // scroll the item up by an amount equal to 80% the container nav's height
992      api.scrollTo(0, selectedOffset - ($nav.height() * .8), false);
993    }
994  }
995}
996
997
998
999
1000
1001
1002/* Show popup dialogs */
1003function showDialog(id) {
1004  $dialog = $("#"+id);
1005  $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>');
1006  $dialog.wrapInner('<div/>');
1007  $dialog.removeClass("hide");
1008}
1009
1010
1011
1012
1013
1014/* #########    COOKIES!     ########## */
1015
1016function readCookie(cookie) {
1017  var myCookie = cookie_namespace+"_"+cookie+"=";
1018  if (document.cookie) {
1019    var index = document.cookie.indexOf(myCookie);
1020    if (index != -1) {
1021      var valStart = index + myCookie.length;
1022      var valEnd = document.cookie.indexOf(";", valStart);
1023      if (valEnd == -1) {
1024        valEnd = document.cookie.length;
1025      }
1026      var val = document.cookie.substring(valStart, valEnd);
1027      return val;
1028    }
1029  }
1030  return 0;
1031}
1032
1033function writeCookie(cookie, val, section) {
1034  if (val==undefined) return;
1035  section = section == null ? "_" : "_"+section+"_";
1036  var age = 2*365*24*60*60; // set max-age to 2 years
1037  var cookieValue = cookie_namespace + section + cookie + "=" + val
1038                    + "; max-age=" + age +"; path=/";
1039  document.cookie = cookieValue;
1040}
1041
1042/* #########     END COOKIES!     ########## */
1043
1044
1045var sticky = false;
1046var stickyTop;
1047var prevScrollLeft = 0; // used to compare current position to previous position of horiz scroll
1048/* Sets the vertical scoll position at which the sticky bar should appear.
1049   This method is called to reset the position when search results appear or hide */
1050function setStickyTop() {
1051  stickyTop = $('#header-wrapper').outerHeight() - $('#header > .dac-header-inner').outerHeight();
1052}
1053
1054/*
1055 * Displays sticky nav bar on pages when dac header scrolls out of view
1056 */
1057$(window).scroll(function(event) {
1058  // Exit if the mouse target is a DIV, because that means the event is coming
1059  // from a scrollable div and so there's no need to make adjustments to our layout
1060  if ($(event.target).nodeName == "DIV") {
1061    return;
1062  }
1063
1064  checkSticky();
1065});
1066
1067function checkSticky() {
1068  setStickyTop();
1069  var $headerEl = $('#header');
1070  // Exit if there's no sidenav
1071  if ($('#side-nav').length == 0) return;
1072
1073  var top = $(window).scrollTop();
1074  // we set the navbar fixed when the scroll position is beyond the height of the site header...
1075  var shouldBeSticky = top > stickyTop;
1076  // ... except if the document content is shorter than the sidenav height.
1077  // (this is necessary to avoid crazy behavior on OSX Lion due to overscroll bouncing)
1078  if ($("#doc-col").height() < $("#side-nav").height()) {
1079    shouldBeSticky = false;
1080  }
1081  // Nor on mobile
1082  if (window.innerWidth < 720) {
1083    shouldBeSticky = false;
1084  }
1085  // Account for horizontal scroll
1086  var scrollLeft = $(window).scrollLeft();
1087  // When the sidenav is fixed and user scrolls horizontally, reposition the sidenav to match
1088  if (sticky && (scrollLeft != prevScrollLeft)) {
1089    updateSideNavDimensions();
1090    prevScrollLeft = scrollLeft;
1091  }
1092
1093  // Don't continue if the header is sufficently far away
1094  // (to avoid intensive resizing that slows scrolling)
1095  if (sticky == shouldBeSticky) {
1096    return;
1097  }
1098
1099  // If sticky header visible and position is now near top, hide sticky
1100  if (sticky && !shouldBeSticky) {
1101    sticky = false;
1102    // make the sidenav static again
1103    $('#devdoc-nav')
1104      .removeClass('fixed')
1105      .css({'width':'auto','margin':''});
1106    // delay hide the sticky
1107    $headerEl.removeClass('is-sticky');
1108
1109    // update the sidenaav position for side scrolling
1110    updateSideNavDimensions();
1111  } else if (!sticky && shouldBeSticky) {
1112    sticky = true;
1113    $headerEl.addClass('is-sticky');
1114
1115    // make the sidenav fixed
1116    $('#devdoc-nav')
1117      .addClass('fixed');
1118
1119    // update the sidenaav position for side scrolling
1120    updateSideNavDimensions();
1121
1122  }
1123  resizeNav(250); // pass true in order to delay the scrollbar re-initialization for performance
1124}
1125
1126/*
1127 * Manages secion card states and nav resize to conclude loading
1128 */
1129(function() {
1130  $(document).ready(function() {
1131
1132    // Stack hover states
1133    $('.section-card-menu').each(function(index, el) {
1134      var height = $(el).height();
1135      $(el).css({height:height+'px', position:'relative'});
1136      var $cardInfo = $(el).find('.card-info');
1137
1138      $cardInfo.css({position: 'absolute', bottom:'0px', left:'0px', right:'0px', overflow:'visible'});
1139    });
1140
1141  });
1142
1143})();
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158/*      MISC LIBRARY FUNCTIONS     */
1159
1160
1161
1162
1163
1164function toggle(obj, slide) {
1165  var ul = $("ul:first", obj);
1166  var li = ul.parent();
1167  if (li.hasClass("closed")) {
1168    if (slide) {
1169      ul.slideDown("fast");
1170    } else {
1171      ul.show();
1172    }
1173    li.removeClass("closed");
1174    li.addClass("open");
1175    $(".toggle-img", li).attr("title", "hide pages");
1176  } else {
1177    ul.slideUp("fast");
1178    li.removeClass("open");
1179    li.addClass("closed");
1180    $(".toggle-img", li).attr("title", "show pages");
1181  }
1182}
1183
1184
1185function buildToggleLists() {
1186  $(".toggle-list").each(
1187    function(i) {
1188      $("div:first", this).append("<a class='toggle-img' href='#' title='show pages' onClick='toggle(this.parentNode.parentNode, true); return false;'></a>");
1189      $(this).addClass("closed");
1190    });
1191}
1192
1193
1194
1195function hideNestedItems(list, toggle) {
1196  $list = $(list);
1197  // hide nested lists
1198  if($list.hasClass('showing')) {
1199    $("li ol", $list).hide('fast');
1200    $list.removeClass('showing');
1201  // show nested lists
1202  } else {
1203    $("li ol", $list).show('fast');
1204    $list.addClass('showing');
1205  }
1206  $(".more,.less",$(toggle)).toggle();
1207}
1208
1209
1210/* Call this to add listeners to a <select> element for Studio/Eclipse/Other docs */
1211function setupIdeDocToggle() {
1212  $( "select.ide" ).change(function() {
1213    var selected = $(this).find("option:selected").attr("value");
1214    $(".select-ide").hide();
1215    $(".select-ide."+selected).show();
1216
1217    $("select.ide").val(selected);
1218  });
1219}
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244/*      REFERENCE NAV SWAP     */
1245
1246
1247function getNavPref() {
1248  var v = readCookie('reference_nav');
1249  if (v != NAV_PREF_TREE) {
1250    v = NAV_PREF_PANELS;
1251  }
1252  return v;
1253}
1254
1255function chooseDefaultNav() {
1256  nav_pref = getNavPref();
1257  if (nav_pref == NAV_PREF_TREE) {
1258    $("#nav-panels").toggle();
1259    $("#panel-link").toggle();
1260    $("#nav-tree").toggle();
1261    $("#tree-link").toggle();
1262  }
1263}
1264
1265function swapNav() {
1266  if (nav_pref == NAV_PREF_TREE) {
1267    nav_pref = NAV_PREF_PANELS;
1268  } else {
1269    nav_pref = NAV_PREF_TREE;
1270    init_default_navtree(toRoot);
1271  }
1272  writeCookie("nav", nav_pref, "reference");
1273
1274  $("#nav-panels").toggle();
1275  $("#panel-link").toggle();
1276  $("#nav-tree").toggle();
1277  $("#tree-link").toggle();
1278
1279  resizeNav();
1280
1281  // Gross nasty hack to make tree view show up upon first swap by setting height manually
1282  $("#nav-tree .jspContainer:visible")
1283      .css({'height':$("#nav-tree .jspContainer .jspPane").height() +'px'});
1284  // Another nasty hack to make the scrollbar appear now that we have height
1285  resizeNav();
1286
1287  if ($("#nav-tree").is(':visible')) {
1288    scrollIntoView("nav-tree");
1289  } else {
1290    scrollIntoView("packages-nav");
1291    scrollIntoView("classes-nav");
1292  }
1293}
1294
1295
1296
1297/* ############################################ */
1298/* ##########     LOCALIZATION     ############ */
1299/* ############################################ */
1300
1301function getBaseUri(uri) {
1302  var intlUrl = (uri.substring(0,6) == "/intl/");
1303  if (intlUrl) {
1304    base = uri.substring(uri.indexOf('intl/')+5,uri.length);
1305    base = base.substring(base.indexOf('/')+1, base.length);
1306      //alert("intl, returning base url: /" + base);
1307    return ("/" + base);
1308  } else {
1309      //alert("not intl, returning uri as found.");
1310    return uri;
1311  }
1312}
1313
1314function requestAppendHL(uri) {
1315//append "?hl=<lang> to an outgoing request (such as to blog)
1316  var lang = getLangPref();
1317  if (lang) {
1318    var q = 'hl=' + lang;
1319    uri += '?' + q;
1320    window.location = uri;
1321    return false;
1322  } else {
1323    return true;
1324  }
1325}
1326
1327
1328function changeNavLang(lang) {
1329  if (lang === 'en') { return; }
1330
1331  var $links = $('a[' + lang + '-lang]');
1332  $links.each(function(){ // for each link with a translation
1333    var $link = $(this);
1334    // put the desired language from the attribute as the text
1335    $link.text($link.attr(lang + '-lang'))
1336  });
1337}
1338
1339function changeLangPref(lang, submit) {
1340  writeCookie("pref_lang", lang, null);
1341
1342  //  #######  TODO:  Remove this condition once we're stable on devsite #######
1343  //  This condition is only needed if we still need to support legacy GAE server
1344  if (devsite) {
1345    // Switch language when on Devsite server
1346    if (submit) {
1347      $("#setlang").submit();
1348    }
1349  } else {
1350    // Switch language when on legacy GAE server
1351    if (submit) {
1352      window.location = getBaseUri(location.pathname);
1353    }
1354  }
1355}
1356
1357function loadLangPref() {
1358  var lang = readCookie("pref_lang");
1359  if (lang != 0) {
1360    $("#language").find("option[value='"+lang+"']").attr("selected",true);
1361  }
1362}
1363
1364function getLangPref() {
1365  var lang = $("#language").find(":selected").attr("value");
1366  if (!lang) {
1367    lang = readCookie("pref_lang");
1368  }
1369  return (lang != 0) ? lang : 'en';
1370}
1371
1372/* ##########     END LOCALIZATION     ############ */
1373
1374
1375
1376
1377
1378
1379/* Used to hide and reveal supplemental content, such as long code samples.
1380   See the companion CSS in android-developer-docs.css */
1381function toggleContent(obj) {
1382  var div = $(obj).closest(".toggle-content");
1383  var toggleMe = $(".toggle-content-toggleme:eq(0)",div);
1384  if (div.hasClass("closed")) { // if it's closed, open it
1385    toggleMe.slideDown();
1386    $(".toggle-content-text:eq(0)", obj).toggle();
1387    div.removeClass("closed").addClass("open");
1388    $(".toggle-content-img:eq(0)", div).attr("title", "hide").attr("src", toRoot
1389                  + "assets/images/triangle-opened.png");
1390  } else { // if it's open, close it
1391    toggleMe.slideUp('fast', function() {  // Wait until the animation is done before closing arrow
1392      $(".toggle-content-text:eq(0)", obj).toggle();
1393      div.removeClass("open").addClass("closed");
1394      div.find(".toggle-content").removeClass("open").addClass("closed")
1395              .find(".toggle-content-toggleme").hide();
1396      $(".toggle-content-img", div).attr("title", "show").attr("src", toRoot
1397                  + "assets/images/triangle-closed.png");
1398    });
1399  }
1400  return false;
1401}
1402
1403
1404/* New version of expandable content */
1405function toggleExpandable(link,id) {
1406  if($(id).is(':visible')) {
1407    $(id).slideUp();
1408    $(link).removeClass('expanded');
1409  } else {
1410    $(id).slideDown();
1411    $(link).addClass('expanded');
1412  }
1413}
1414
1415function hideExpandable(ids) {
1416  $(ids).slideUp();
1417  $(ids).prev('h4').find('a.expandable').removeClass('expanded');
1418}
1419
1420
1421
1422
1423
1424/*
1425 *  Slideshow 1.0
1426 *  Used on /index.html and /develop/index.html for carousel
1427 *
1428 *  Sample usage:
1429 *  HTML -
1430 *  <div class="slideshow-container">
1431 *   <a href="" class="slideshow-prev">Prev</a>
1432 *   <a href="" class="slideshow-next">Next</a>
1433 *   <ul>
1434 *       <li class="item"><img src="images/marquee1.jpg"></li>
1435 *       <li class="item"><img src="images/marquee2.jpg"></li>
1436 *       <li class="item"><img src="images/marquee3.jpg"></li>
1437 *       <li class="item"><img src="images/marquee4.jpg"></li>
1438 *   </ul>
1439 *  </div>
1440 *
1441 *   <script type="text/javascript">
1442 *   $('.slideshow-container').dacSlideshow({
1443 *       auto: true,
1444 *       btnPrev: '.slideshow-prev',
1445 *       btnNext: '.slideshow-next'
1446 *   });
1447 *   </script>
1448 *
1449 *  Options:
1450 *  btnPrev:    optional identifier for previous button
1451 *  btnNext:    optional identifier for next button
1452 *  btnPause:   optional identifier for pause button
1453 *  auto:       whether or not to auto-proceed
1454 *  speed:      animation speed
1455 *  autoTime:   time between auto-rotation
1456 *  easing:     easing function for transition
1457 *  start:      item to select by default
1458 *  scroll:     direction to scroll in
1459 *  pagination: whether or not to include dotted pagination
1460 *
1461 */
1462
1463 (function($) {
1464 $.fn.dacSlideshow = function(o) {
1465
1466     //Options - see above
1467     o = $.extend({
1468         btnPrev:   null,
1469         btnNext:   null,
1470         btnPause:  null,
1471         auto:      true,
1472         speed:     500,
1473         autoTime:  12000,
1474         easing:    null,
1475         start:     0,
1476         scroll:    1,
1477         pagination: true
1478
1479     }, o || {});
1480
1481     //Set up a carousel for each
1482     return this.each(function() {
1483
1484         var running = false;
1485         var animCss = o.vertical ? "top" : "left";
1486         var sizeCss = o.vertical ? "height" : "width";
1487         var div = $(this);
1488         var ul = $("ul", div);
1489         var tLi = $("li", ul);
1490         var tl = tLi.size();
1491         var timer = null;
1492
1493         var li = $("li", ul);
1494         var itemLength = li.size();
1495         var curr = o.start;
1496
1497         li.css({float: o.vertical ? "none" : "left"});
1498         ul.css({margin: "0", padding: "0", position: "relative", "list-style-type": "none", "z-index": "1"});
1499         div.css({position: "relative", "z-index": "2", left: "0px"});
1500
1501         var liSize = o.vertical ? height(li) : width(li);
1502         var ulSize = liSize * itemLength;
1503         var divSize = liSize;
1504
1505         li.css({width: li.width(), height: li.height()});
1506         ul.css(sizeCss, ulSize+"px").css(animCss, -(curr*liSize));
1507
1508         div.css(sizeCss, divSize+"px");
1509
1510         //Pagination
1511         if (o.pagination) {
1512             var pagination = $("<div class='pagination'></div>");
1513             var pag_ul = $("<ul></ul>");
1514             if (tl > 1) {
1515               for (var i=0;i<tl;i++) {
1516                    var li = $("<li>"+i+"</li>");
1517                    pag_ul.append(li);
1518                    if (i==o.start) li.addClass('active');
1519                        li.click(function() {
1520                        go(parseInt($(this).text()));
1521                    })
1522                }
1523                pagination.append(pag_ul);
1524                div.append(pagination);
1525             }
1526         }
1527
1528         //Previous button
1529         if(o.btnPrev)
1530             $(o.btnPrev).click(function(e) {
1531                 e.preventDefault();
1532                 return go(curr-o.scroll);
1533             });
1534
1535         //Next button
1536         if(o.btnNext)
1537             $(o.btnNext).click(function(e) {
1538                 e.preventDefault();
1539                 return go(curr+o.scroll);
1540             });
1541
1542         //Pause button
1543         if(o.btnPause)
1544             $(o.btnPause).click(function(e) {
1545                 e.preventDefault();
1546                 if ($(this).hasClass('paused')) {
1547                     startRotateTimer();
1548                 } else {
1549                     pauseRotateTimer();
1550                 }
1551             });
1552
1553         //Auto rotation
1554         if(o.auto) startRotateTimer();
1555
1556         function startRotateTimer() {
1557             clearInterval(timer);
1558             timer = setInterval(function() {
1559                  if (curr == tl-1) {
1560                    go(0);
1561                  } else {
1562                    go(curr+o.scroll);
1563                  }
1564              }, o.autoTime);
1565             $(o.btnPause).removeClass('paused');
1566         }
1567
1568         function pauseRotateTimer() {
1569             clearInterval(timer);
1570             $(o.btnPause).addClass('paused');
1571         }
1572
1573         //Go to an item
1574         function go(to) {
1575             if(!running) {
1576
1577                 if(to<0) {
1578                    to = itemLength-1;
1579                 } else if (to>itemLength-1) {
1580                    to = 0;
1581                 }
1582                 curr = to;
1583
1584                 running = true;
1585
1586                 ul.animate(
1587                     animCss == "left" ? { left: -(curr*liSize) } : { top: -(curr*liSize) } , o.speed, o.easing,
1588                     function() {
1589                         running = false;
1590                     }
1591                 );
1592
1593                 $(o.btnPrev + "," + o.btnNext).removeClass("disabled");
1594                 $( (curr-o.scroll<0 && o.btnPrev)
1595                     ||
1596                    (curr+o.scroll > itemLength && o.btnNext)
1597                     ||
1598                    []
1599                  ).addClass("disabled");
1600
1601
1602                 var nav_items = $('li', pagination);
1603                 nav_items.removeClass('active');
1604                 nav_items.eq(to).addClass('active');
1605
1606
1607             }
1608             if(o.auto) startRotateTimer();
1609             return false;
1610         };
1611     });
1612 };
1613
1614 function css(el, prop) {
1615     return parseInt($.css(el[0], prop)) || 0;
1616 };
1617 function width(el) {
1618     return  el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
1619 };
1620 function height(el) {
1621     return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
1622 };
1623
1624 })(jQuery);
1625
1626
1627/*
1628 *  dacSlideshow 1.0
1629 *  Used on develop/index.html for side-sliding tabs
1630 *
1631 *  Sample usage:
1632 *  HTML -
1633 *  <div class="slideshow-container">
1634 *   <a href="" class="slideshow-prev">Prev</a>
1635 *   <a href="" class="slideshow-next">Next</a>
1636 *   <ul>
1637 *       <li class="item"><img src="images/marquee1.jpg"></li>
1638 *       <li class="item"><img src="images/marquee2.jpg"></li>
1639 *       <li class="item"><img src="images/marquee3.jpg"></li>
1640 *       <li class="item"><img src="images/marquee4.jpg"></li>
1641 *   </ul>
1642 *  </div>
1643 *
1644 *   <script type="text/javascript">
1645 *   $('.slideshow-container').dacSlideshow({
1646 *       auto: true,
1647 *       btnPrev: '.slideshow-prev',
1648 *       btnNext: '.slideshow-next'
1649 *   });
1650 *   </script>
1651 *
1652 *  Options:
1653 *  btnPrev:    optional identifier for previous button
1654 *  btnNext:    optional identifier for next button
1655 *  auto:       whether or not to auto-proceed
1656 *  speed:      animation speed
1657 *  autoTime:   time between auto-rotation
1658 *  easing:     easing function for transition
1659 *  start:      item to select by default
1660 *  scroll:     direction to scroll in
1661 *  pagination: whether or not to include dotted pagination
1662 *
1663 */
1664 (function($) {
1665 $.fn.dacTabbedList = function(o) {
1666
1667     //Options - see above
1668     o = $.extend({
1669         speed : 250,
1670         easing: null,
1671         nav_id: null,
1672         frame_id: null
1673     }, o || {});
1674
1675     //Set up a carousel for each
1676     return this.each(function() {
1677
1678         var curr = 0;
1679         var running = false;
1680         var animCss = "margin-left";
1681         var sizeCss = "width";
1682         var div = $(this);
1683
1684         var nav = $(o.nav_id, div);
1685         var nav_li = $("li", nav);
1686         var nav_size = nav_li.size();
1687         var frame = div.find(o.frame_id);
1688         var content_width = $(frame).find('ul').width();
1689         //Buttons
1690         $(nav_li).click(function(e) {
1691           go($(nav_li).index($(this)));
1692         })
1693
1694         //Go to an item
1695         function go(to) {
1696             if(!running) {
1697                 curr = to;
1698                 running = true;
1699
1700                 frame.animate({ 'margin-left' : -(curr*content_width) }, o.speed, o.easing,
1701                     function() {
1702                         running = false;
1703                     }
1704                 );
1705
1706
1707                 nav_li.removeClass('active');
1708                 nav_li.eq(to).addClass('active');
1709
1710
1711             }
1712             return false;
1713         };
1714     });
1715 };
1716
1717 function css(el, prop) {
1718     return parseInt($.css(el[0], prop)) || 0;
1719 };
1720 function width(el) {
1721     return  el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
1722 };
1723 function height(el) {
1724     return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
1725 };
1726
1727 })(jQuery);
1728
1729
1730
1731
1732
1733/* ######################################################## */
1734/* ################  SEARCH SUGGESTIONS  ################## */
1735/* ######################################################## */
1736
1737
1738
1739var gSelectedIndex = -1;  // the index position of currently highlighted suggestion
1740var gSelectedColumn = -1;  // which column of suggestion lists is currently focused
1741
1742var gMatches = new Array();
1743var gLastText = "";
1744var gInitialized = false;
1745var ROW_COUNT_FRAMEWORK = 20;       // max number of results in list
1746var gListLength = 0;
1747
1748
1749var gGoogleMatches = new Array();
1750var ROW_COUNT_GOOGLE = 15;          // max number of results in list
1751var gGoogleListLength = 0;
1752
1753var gDocsMatches = new Array();
1754var ROW_COUNT_DOCS = 100;          // max number of results in list
1755var gDocsListLength = 0;
1756
1757function onSuggestionClick(link) {
1758  // When user clicks a suggested document, track it
1759  ga('send', 'event', 'Suggestion Click', 'clicked: ' + $(link).attr('href'),
1760                'query: ' + $("#search_autocomplete").val().toLowerCase());
1761}
1762
1763function set_item_selected($li, selected)
1764{
1765    if (selected) {
1766        $li.attr('class','jd-autocomplete jd-selected');
1767    } else {
1768        $li.attr('class','jd-autocomplete');
1769    }
1770}
1771
1772function set_item_values(toroot, $li, match)
1773{
1774    var $link = $('a',$li);
1775    $link.html(match.__hilabel || match.label);
1776    $link.attr('href',toroot + match.link);
1777}
1778
1779function set_item_values_jd(toroot, $li, match)
1780{
1781    var $link = $('a',$li);
1782    $link.html(match.title);
1783    $link.attr('href',toroot + match.url);
1784}
1785
1786function new_suggestion($list) {
1787    var $li = $("<li class='jd-autocomplete'></li>");
1788    $list.append($li);
1789
1790    $li.mousedown(function() {
1791        window.location = this.firstChild.getAttribute("href");
1792    });
1793    $li.mouseover(function() {
1794        $('.search_filtered_wrapper li').removeClass('jd-selected');
1795        $(this).addClass('jd-selected');
1796        gSelectedColumn = $(".search_filtered:visible").index($(this).closest('.search_filtered'));
1797        gSelectedIndex = $("li", $(".search_filtered:visible")[gSelectedColumn]).index(this);
1798    });
1799    $li.append("<a onclick='onSuggestionClick(this)'></a>");
1800    $li.attr('class','show-item');
1801    return $li;
1802}
1803
1804function sync_selection_table(toroot)
1805{
1806    var $li; //list item jquery object
1807    var i; //list item iterator
1808
1809    // if there are NO results at all, hide all columns
1810    if (!(gMatches.length > 0) && !(gGoogleMatches.length > 0) && !(gDocsMatches.length > 0)) {
1811        $('.suggest-card').hide(300);
1812        return;
1813    }
1814
1815    // if there are api results
1816    if ((gMatches.length > 0) || (gGoogleMatches.length > 0)) {
1817      // reveal suggestion list
1818      $('.suggest-card.reference').show();
1819      var listIndex = 0; // list index position
1820
1821      // reset the lists
1822      $(".suggest-card.reference li").remove();
1823
1824      // ########### ANDROID RESULTS #############
1825      if (gMatches.length > 0) {
1826
1827          // determine android results to show
1828          gListLength = gMatches.length < ROW_COUNT_FRAMEWORK ?
1829                        gMatches.length : ROW_COUNT_FRAMEWORK;
1830          for (i=0; i<gListLength; i++) {
1831              var $li = new_suggestion($(".suggest-card.reference ul"));
1832              set_item_values(toroot, $li, gMatches[i]);
1833              set_item_selected($li, i == gSelectedIndex);
1834          }
1835      }
1836
1837      // ########### GOOGLE RESULTS #############
1838      if (gGoogleMatches.length > 0) {
1839          // show header for list
1840          $(".suggest-card.reference ul").append("<li class='header'>in Google Services:</li>");
1841
1842          // determine google results to show
1843          gGoogleListLength = gGoogleMatches.length < ROW_COUNT_GOOGLE ? gGoogleMatches.length : ROW_COUNT_GOOGLE;
1844          for (i=0; i<gGoogleListLength; i++) {
1845              var $li = new_suggestion($(".suggest-card.reference ul"));
1846              set_item_values(toroot, $li, gGoogleMatches[i]);
1847              set_item_selected($li, i == gSelectedIndex);
1848          }
1849      }
1850    } else {
1851      $('.suggest-card.reference').hide();
1852    }
1853
1854    // ########### JD DOC RESULTS #############
1855    if (gDocsMatches.length > 0) {
1856        // reset the lists
1857        $(".suggest-card:not(.reference) li").remove();
1858
1859        // determine google results to show
1860        // NOTE: The order of the conditions below for the sugg.type MUST BE SPECIFIC:
1861        // The order must match the reverse order that each section appears as a card in
1862        // the suggestion UI... this may be only for the "develop" grouped items though.
1863        gDocsListLength = gDocsMatches.length < ROW_COUNT_DOCS ? gDocsMatches.length : ROW_COUNT_DOCS;
1864        for (i=0; i<gDocsListLength; i++) {
1865            var sugg = gDocsMatches[i];
1866            var $li;
1867            if (sugg.type == "design") {
1868                $li = new_suggestion($(".suggest-card.design ul"));
1869            } else
1870            if (sugg.type == "distribute") {
1871                $li = new_suggestion($(".suggest-card.distribute ul"));
1872            } else
1873            if (sugg.type == "samples") {
1874                $li = new_suggestion($(".suggest-card.develop .child-card.samples"));
1875            } else
1876            if (sugg.type == "training") {
1877                $li = new_suggestion($(".suggest-card.develop .child-card.training"));
1878            } else
1879            if (sugg.type == "about"||"guide"||"tools"||"google") {
1880                $li = new_suggestion($(".suggest-card.develop .child-card.guides"));
1881            } else {
1882              continue;
1883            }
1884
1885            set_item_values_jd(toroot, $li, sugg);
1886            set_item_selected($li, i == gSelectedIndex);
1887        }
1888
1889        // add heading and show or hide card
1890        if ($(".suggest-card.design li").length > 0) {
1891          $(".suggest-card.design ul").prepend("<li class='header'>Design:</li>");
1892          $(".suggest-card.design").show(300);
1893        } else {
1894          $('.suggest-card.design').hide(300);
1895        }
1896        if ($(".suggest-card.distribute li").length > 0) {
1897          $(".suggest-card.distribute ul").prepend("<li class='header'>Distribute:</li>");
1898          $(".suggest-card.distribute").show(300);
1899        } else {
1900          $('.suggest-card.distribute').hide(300);
1901        }
1902        if ($(".child-card.guides li").length > 0) {
1903          $(".child-card.guides").prepend("<li class='header'>Guides:</li>");
1904          $(".child-card.guides li").appendTo(".suggest-card.develop ul");
1905        }
1906        if ($(".child-card.training li").length > 0) {
1907          $(".child-card.training").prepend("<li class='header'>Training:</li>");
1908          $(".child-card.training li").appendTo(".suggest-card.develop ul");
1909        }
1910        if ($(".child-card.samples li").length > 0) {
1911          $(".child-card.samples").prepend("<li class='header'>Samples:</li>");
1912          $(".child-card.samples li").appendTo(".suggest-card.develop ul");
1913        }
1914
1915        if ($(".suggest-card.develop li").length > 0) {
1916          $(".suggest-card.develop").show(300);
1917        } else {
1918          $('.suggest-card.develop').hide(300);
1919        }
1920
1921    } else {
1922      $('.suggest-card:not(.reference)').hide(300);
1923    }
1924}
1925
1926/** Called by the search input's onkeydown and onkeyup events.
1927  * Handles navigation with keyboard arrows, Enter key to invoke search,
1928  * otherwise invokes search suggestions on key-up event.
1929  * @param e       The JS event
1930  * @param kd      True if the event is key-down
1931  * @param toroot  A string for the site's root path
1932  * @returns       True if the event should bubble up
1933  */
1934function search_changed(e, kd, toroot)
1935{
1936    var currentLang = getLangPref();
1937    var search = document.getElementById("search_autocomplete");
1938    var text = search.value.replace(/(^ +)|( +$)/g, '');
1939    // get the ul hosting the currently selected item
1940    gSelectedColumn = gSelectedColumn >= 0 ? gSelectedColumn :  0;
1941    var $columns = $(".search_filtered_wrapper").find(".search_filtered:visible");
1942    var $selectedUl = $columns[gSelectedColumn];
1943
1944    // show/hide the close button
1945    if (text != '') {
1946        $("#search-close").removeClass("hide");
1947    } else {
1948        $("#search-close").addClass("hide");
1949    }
1950    // 27 = esc
1951    if (e.keyCode == 27) {
1952        // close all search results
1953        if (kd) $('#search-close').trigger('click');
1954        return true;
1955    }
1956    // 13 = enter
1957    else if (e.keyCode == 13) {
1958        if (gSelectedIndex < 0) {
1959            $('.suggest-card').hide();
1960            if ($("#searchResults").is(":hidden") && (search.value != "")) {
1961              // if results aren't showing (and text not empty), return true to allow search to execute
1962              $('body,html').animate({scrollTop:0}, '500', 'swing');
1963              return true;
1964            } else {
1965              // otherwise, results are already showing, so allow ajax to auto refresh the results
1966              // and ignore this Enter press to avoid the reload.
1967              return false;
1968            }
1969        } else if (kd && gSelectedIndex >= 0) {
1970            // click the link corresponding to selected item
1971            $("a",$("li",$selectedUl)[gSelectedIndex]).get()[0].click();
1972            return false;
1973        }
1974    }
1975    // If Google results are showing, return true to allow ajax search to execute
1976    else if ($("#searchResults").is(":visible")) {
1977        // Also, if search_results is scrolled out of view, scroll to top to make results visible
1978        if ((sticky ) && (search.value != "")) {
1979          $('body,html').animate({scrollTop:0}, '500', 'swing');
1980        }
1981        return true;
1982    }
1983    // 38 UP ARROW
1984    else if (kd && (e.keyCode == 38)) {
1985        // if the next item is a header, skip it
1986        if ($($("li", $selectedUl)[gSelectedIndex-1]).hasClass("header")) {
1987            gSelectedIndex--;
1988        }
1989        if (gSelectedIndex >= 0) {
1990            $('li', $selectedUl).removeClass('jd-selected');
1991            gSelectedIndex--;
1992            $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
1993            // If user reaches top, reset selected column
1994            if (gSelectedIndex < 0) {
1995              gSelectedColumn = -1;
1996            }
1997        }
1998        return false;
1999    }
2000    // 40 DOWN ARROW
2001    else if (kd && (e.keyCode == 40)) {
2002        // if the next item is a header, skip it
2003        if ($($("li", $selectedUl)[gSelectedIndex+1]).hasClass("header")) {
2004            gSelectedIndex++;
2005        }
2006        if ((gSelectedIndex < $("li", $selectedUl).length-1) ||
2007                        ($($("li", $selectedUl)[gSelectedIndex+1]).hasClass("header"))) {
2008            $('li', $selectedUl).removeClass('jd-selected');
2009            gSelectedIndex++;
2010            $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
2011        }
2012        return false;
2013    }
2014    // Consider left/right arrow navigation
2015    // NOTE: Order of suggest columns are reverse order (index position 0 is on right)
2016    else if (kd && $columns.length > 1 && gSelectedColumn >= 0) {
2017      // 37 LEFT ARROW
2018      // go left only if current column is not left-most column (last column)
2019      if (e.keyCode == 37 && gSelectedColumn < $columns.length - 1) {
2020        $('li', $selectedUl).removeClass('jd-selected');
2021        gSelectedColumn++;
2022        $selectedUl = $columns[gSelectedColumn];
2023        // keep or reset the selected item to last item as appropriate
2024        gSelectedIndex = gSelectedIndex >
2025                $("li", $selectedUl).length-1 ?
2026                $("li", $selectedUl).length-1 : gSelectedIndex;
2027        // if the corresponding item is a header, move down
2028        if ($($("li", $selectedUl)[gSelectedIndex]).hasClass("header")) {
2029          gSelectedIndex++;
2030        }
2031        // set item selected
2032        $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
2033        return false;
2034      }
2035      // 39 RIGHT ARROW
2036      // go right only if current column is not the right-most column (first column)
2037      else if (e.keyCode == 39 && gSelectedColumn > 0) {
2038        $('li', $selectedUl).removeClass('jd-selected');
2039        gSelectedColumn--;
2040        $selectedUl = $columns[gSelectedColumn];
2041        // keep or reset the selected item to last item as appropriate
2042        gSelectedIndex = gSelectedIndex >
2043                $("li", $selectedUl).length-1 ?
2044                $("li", $selectedUl).length-1 : gSelectedIndex;
2045        // if the corresponding item is a header, move down
2046        if ($($("li", $selectedUl)[gSelectedIndex]).hasClass("header")) {
2047          gSelectedIndex++;
2048        }
2049        // set item selected
2050        $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected');
2051        return false;
2052      }
2053    }
2054
2055    // if key-up event and not arrow down/up/left/right,
2056    // read the search query and add suggestions to gMatches
2057    else if (!kd && (e.keyCode != 40)
2058                 && (e.keyCode != 38)
2059                 && (e.keyCode != 37)
2060                 && (e.keyCode != 39)) {
2061        gSelectedIndex = -1;
2062        gMatches = new Array();
2063        matchedCount = 0;
2064        gGoogleMatches = new Array();
2065        matchedCountGoogle = 0;
2066        gDocsMatches = new Array();
2067        matchedCountDocs = 0;
2068
2069        // Search for Android matches
2070        for (var i=0; i<DATA.length; i++) {
2071            var s = DATA[i];
2072            if (text.length != 0 &&
2073                  s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) {
2074                gMatches[matchedCount] = s;
2075                matchedCount++;
2076            }
2077        }
2078        rank_autocomplete_api_results(text, gMatches);
2079        for (var i=0; i<gMatches.length; i++) {
2080            var s = gMatches[i];
2081        }
2082
2083
2084        // Search for Google matches
2085        for (var i=0; i<GOOGLE_DATA.length; i++) {
2086            var s = GOOGLE_DATA[i];
2087            if (text.length != 0 &&
2088                  s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) {
2089                gGoogleMatches[matchedCountGoogle] = s;
2090                matchedCountGoogle++;
2091            }
2092        }
2093        rank_autocomplete_api_results(text, gGoogleMatches);
2094        for (var i=0; i<gGoogleMatches.length; i++) {
2095            var s = gGoogleMatches[i];
2096        }
2097
2098        highlight_autocomplete_result_labels(text);
2099
2100
2101
2102        // Search for matching JD docs
2103        if (text.length >= 2) {
2104          // match only the beginning of a word
2105          var queryStr = text.toLowerCase();
2106
2107          // Search for Training classes
2108          for (var i=0; i<TRAINING_RESOURCES.length; i++) {
2109            // current search comparison, with counters for tag and title,
2110            // used later to improve ranking
2111            var s = TRAINING_RESOURCES[i];
2112            s.matched_tag = 0;
2113            s.matched_title = 0;
2114            var matched = false;
2115
2116            // Check if query matches any tags; work backwards toward 1 to assist ranking
2117            for (var j = s.keywords.length - 1; j >= 0; j--) {
2118              // it matches a tag
2119              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2120                matched = true;
2121                s.matched_tag = j + 1; // add 1 to index position
2122              }
2123            }
2124            // Don't consider doc title for lessons (only for class landing pages),
2125            // unless the lesson has a tag that already matches
2126            if ((s.lang == currentLang) &&
2127                  (!(s.type == "training" && s.url.indexOf("index.html") == -1) || matched)) {
2128              // it matches the doc title
2129              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2130                matched = true;
2131                s.matched_title = 1;
2132              }
2133            }
2134            if (matched) {
2135              gDocsMatches[matchedCountDocs] = s;
2136              matchedCountDocs++;
2137            }
2138          }
2139
2140
2141          // Search for API Guides
2142          for (var i=0; i<GUIDE_RESOURCES.length; i++) {
2143            // current search comparison, with counters for tag and title,
2144            // used later to improve ranking
2145            var s = GUIDE_RESOURCES[i];
2146            s.matched_tag = 0;
2147            s.matched_title = 0;
2148            var matched = false;
2149
2150            // Check if query matches any tags; work backwards toward 1 to assist ranking
2151            for (var j = s.keywords.length - 1; j >= 0; j--) {
2152              // it matches a tag
2153              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2154
2155                matched = true;
2156                s.matched_tag = j + 1; // add 1 to index position
2157              }
2158            }
2159            // Check if query matches the doc title, but only for current language
2160            if (s.lang == currentLang) {
2161              // if query matches the doc title
2162              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2163                matched = true;
2164                s.matched_title = 1;
2165              }
2166            }
2167            if (matched) {
2168              gDocsMatches[matchedCountDocs] = s;
2169              matchedCountDocs++;
2170            }
2171          }
2172
2173
2174          // Search for Tools Guides
2175          for (var i=0; i<TOOLS_RESOURCES.length; i++) {
2176            // current search comparison, with counters for tag and title,
2177            // used later to improve ranking
2178            var s = TOOLS_RESOURCES[i];
2179            s.matched_tag = 0;
2180            s.matched_title = 0;
2181            var matched = false;
2182
2183            // Check if query matches any tags; work backwards toward 1 to assist ranking
2184            for (var j = s.keywords.length - 1; j >= 0; j--) {
2185              // it matches a tag
2186                if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2187                matched = true;
2188                s.matched_tag = j + 1; // add 1 to index position
2189              }
2190            }
2191            // Check if query matches the doc title, but only for current language
2192            if (s.lang == currentLang) {
2193              // if query matches the doc title
2194                if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2195                matched = true;
2196                s.matched_title = 1;
2197              }
2198            }
2199            if (matched) {
2200              gDocsMatches[matchedCountDocs] = s;
2201              matchedCountDocs++;
2202            }
2203          }
2204
2205
2206          // Search for About docs
2207          for (var i=0; i<ABOUT_RESOURCES.length; i++) {
2208            // current search comparison, with counters for tag and title,
2209            // used later to improve ranking
2210            var s = ABOUT_RESOURCES[i];
2211            s.matched_tag = 0;
2212            s.matched_title = 0;
2213            var matched = false;
2214
2215            // Check if query matches any tags; work backwards toward 1 to assist ranking
2216            for (var j = s.keywords.length - 1; j >= 0; j--) {
2217              // it matches a tag
2218              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2219                matched = true;
2220                s.matched_tag = j + 1; // add 1 to index position
2221              }
2222            }
2223            // Check if query matches the doc title, but only for current language
2224            if (s.lang == currentLang) {
2225              // if query matches the doc title
2226              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2227                matched = true;
2228                s.matched_title = 1;
2229              }
2230            }
2231            if (matched) {
2232              gDocsMatches[matchedCountDocs] = s;
2233              matchedCountDocs++;
2234            }
2235          }
2236
2237
2238          // Search for Design guides
2239          for (var i=0; i<DESIGN_RESOURCES.length; i++) {
2240            // current search comparison, with counters for tag and title,
2241            // used later to improve ranking
2242            var s = DESIGN_RESOURCES[i];
2243            s.matched_tag = 0;
2244            s.matched_title = 0;
2245            var matched = false;
2246
2247            // Check if query matches any tags; work backwards toward 1 to assist ranking
2248            for (var j = s.keywords.length - 1; j >= 0; j--) {
2249              // it matches a tag
2250              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2251                matched = true;
2252                s.matched_tag = j + 1; // add 1 to index position
2253              }
2254            }
2255            // Check if query matches the doc title, but only for current language
2256            if (s.lang == currentLang) {
2257              // if query matches the doc title
2258              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2259                matched = true;
2260                s.matched_title = 1;
2261              }
2262            }
2263            if (matched) {
2264              gDocsMatches[matchedCountDocs] = s;
2265              matchedCountDocs++;
2266            }
2267          }
2268
2269
2270          // Search for Distribute guides
2271          for (var i=0; i<DISTRIBUTE_RESOURCES.length; i++) {
2272            // current search comparison, with counters for tag and title,
2273            // used later to improve ranking
2274            var s = DISTRIBUTE_RESOURCES[i];
2275            s.matched_tag = 0;
2276            s.matched_title = 0;
2277            var matched = false;
2278
2279            // Check if query matches any tags; work backwards toward 1 to assist ranking
2280            for (var j = s.keywords.length - 1; j >= 0; j--) {
2281              // it matches a tag
2282              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2283                matched = true;
2284                s.matched_tag = j + 1; // add 1 to index position
2285              }
2286            }
2287            // Check if query matches the doc title, but only for current language
2288            if (s.lang == currentLang) {
2289              // if query matches the doc title
2290              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2291                matched = true;
2292                s.matched_title = 1;
2293              }
2294            }
2295            if (matched) {
2296              gDocsMatches[matchedCountDocs] = s;
2297              matchedCountDocs++;
2298            }
2299          }
2300
2301
2302          // Search for Google guides
2303          for (var i=0; i<GOOGLE_RESOURCES.length; i++) {
2304            // current search comparison, with counters for tag and title,
2305            // used later to improve ranking
2306            var s = GOOGLE_RESOURCES[i];
2307            s.matched_tag = 0;
2308            s.matched_title = 0;
2309            var matched = false;
2310
2311            // Check if query matches any tags; work backwards toward 1 to assist ranking
2312            for (var j = s.keywords.length - 1; j >= 0; j--) {
2313              // it matches a tag
2314              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2315                matched = true;
2316                s.matched_tag = j + 1; // add 1 to index position
2317              }
2318            }
2319            // Check if query matches the doc title, but only for current language
2320            if (s.lang == currentLang) {
2321              // if query matches the doc title
2322              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2323                matched = true;
2324                s.matched_title = 1;
2325              }
2326            }
2327            if (matched) {
2328              gDocsMatches[matchedCountDocs] = s;
2329              matchedCountDocs++;
2330            }
2331          }
2332
2333
2334          // Search for Samples
2335          for (var i=0; i<SAMPLES_RESOURCES.length; i++) {
2336            // current search comparison, with counters for tag and title,
2337            // used later to improve ranking
2338            var s = SAMPLES_RESOURCES[i];
2339            s.matched_tag = 0;
2340            s.matched_title = 0;
2341            var matched = false;
2342            // Check if query matches any tags; work backwards toward 1 to assist ranking
2343            for (var j = s.keywords.length - 1; j >= 0; j--) {
2344              // it matches a tag
2345              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2346                matched = true;
2347                s.matched_tag = j + 1; // add 1 to index position
2348              }
2349            }
2350            // Check if query matches the doc title, but only for current language
2351            if (s.lang == currentLang) {
2352              // if query matches the doc title.t
2353              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2354                matched = true;
2355                s.matched_title = 1;
2356              }
2357            }
2358            if (matched) {
2359              gDocsMatches[matchedCountDocs] = s;
2360              matchedCountDocs++;
2361            }
2362          }
2363
2364          // Search for Preview Guides
2365          for (var i=0; i<PREVIEW_RESOURCES.length; i++) {
2366            // current search comparison, with counters for tag and title,
2367            // used later to improve ranking
2368            var s = PREVIEW_RESOURCES[i];
2369            s.matched_tag = 0;
2370            s.matched_title = 0;
2371            var matched = false;
2372
2373            // Check if query matches any tags; work backwards toward 1 to assist ranking
2374            for (var j = s.keywords.length - 1; j >= 0; j--) {
2375              // it matches a tag
2376              if (s.keywords[j].toLowerCase().indexOf(queryStr) == 0) {
2377                matched = true;
2378                s.matched_tag = j + 1; // add 1 to index position
2379              }
2380            }
2381            // Check if query matches the doc title, but only for current language
2382            if (s.lang == currentLang) {
2383              // if query matches the doc title
2384              if (s.title.toLowerCase().indexOf(queryStr) == 0) {
2385                matched = true;
2386                s.matched_title = 1;
2387              }
2388            }
2389            if (matched) {
2390              gDocsMatches[matchedCountDocs] = s;
2391              matchedCountDocs++;
2392            }
2393          }
2394
2395          // Rank/sort all the matched pages
2396          rank_autocomplete_doc_results(text, gDocsMatches);
2397        }
2398
2399        // draw the suggestions
2400        sync_selection_table(toroot);
2401        return true; // allow the event to bubble up to the search api
2402    }
2403}
2404
2405/* Order the jd doc result list based on match quality */
2406function rank_autocomplete_doc_results(query, matches) {
2407    query = query || '';
2408    if (!matches || !matches.length)
2409      return;
2410
2411    var _resultScoreFn = function(match) {
2412        var score = 1.0;
2413
2414        // if the query matched a tag
2415        if (match.matched_tag > 0) {
2416          // multiply score by factor relative to position in tags list (max of 3)
2417          score *= 3 / match.matched_tag;
2418
2419          // if it also matched the title
2420          if (match.matched_title > 0) {
2421            score *= 2;
2422          }
2423        } else if (match.matched_title > 0) {
2424          score *= 3;
2425        }
2426
2427        return score;
2428    };
2429
2430    for (var i=0; i<matches.length; i++) {
2431        matches[i].__resultScore = _resultScoreFn(matches[i]);
2432    }
2433
2434    matches.sort(function(a,b){
2435        var n = b.__resultScore - a.__resultScore;
2436        if (n == 0) // lexicographical sort if scores are the same
2437            n = (a.label < b.label) ? -1 : 1;
2438        return n;
2439    });
2440}
2441
2442/* Order the result list based on match quality */
2443function rank_autocomplete_api_results(query, matches) {
2444    query = query || '';
2445    if (!matches || !matches.length)
2446      return;
2447
2448    // helper function that gets the last occurence index of the given regex
2449    // in the given string, or -1 if not found
2450    var _lastSearch = function(s, re) {
2451      if (s == '')
2452        return -1;
2453      var l = -1;
2454      var tmp;
2455      while ((tmp = s.search(re)) >= 0) {
2456        if (l < 0) l = 0;
2457        l += tmp;
2458        s = s.substr(tmp + 1);
2459      }
2460      return l;
2461    };
2462
2463    // helper function that counts the occurrences of a given character in
2464    // a given string
2465    var _countChar = function(s, c) {
2466      var n = 0;
2467      for (var i=0; i<s.length; i++)
2468        if (s.charAt(i) == c) ++n;
2469      return n;
2470    };
2471
2472    var queryLower = query.toLowerCase();
2473    var queryAlnum = (queryLower.match(/\w+/) || [''])[0];
2474    var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum);
2475    var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b');
2476
2477    var _resultScoreFn = function(result) {
2478        // scores are calculated based on exact and prefix matches,
2479        // and then number of path separators (dots) from the last
2480        // match (i.e. favoring classes and deep package names)
2481        var score = 1.0;
2482        var labelLower = result.label.toLowerCase();
2483        var t;
2484        t = _lastSearch(labelLower, partExactAlnumRE);
2485        if (t >= 0) {
2486            // exact part match
2487            var partsAfter = _countChar(labelLower.substr(t + 1), '.');
2488            score *= 200 / (partsAfter + 1);
2489        } else {
2490            t = _lastSearch(labelLower, partPrefixAlnumRE);
2491            if (t >= 0) {
2492                // part prefix match
2493                var partsAfter = _countChar(labelLower.substr(t + 1), '.');
2494                score *= 20 / (partsAfter + 1);
2495            }
2496        }
2497
2498        return score;
2499    };
2500
2501    for (var i=0; i<matches.length; i++) {
2502        // if the API is deprecated, default score is 0; otherwise, perform scoring
2503        if (matches[i].deprecated == "true") {
2504          matches[i].__resultScore = 0;
2505        } else {
2506          matches[i].__resultScore = _resultScoreFn(matches[i]);
2507        }
2508    }
2509
2510    matches.sort(function(a,b){
2511        var n = b.__resultScore - a.__resultScore;
2512        if (n == 0) // lexicographical sort if scores are the same
2513            n = (a.label < b.label) ? -1 : 1;
2514        return n;
2515    });
2516}
2517
2518/* Add emphasis to part of string that matches query */
2519function highlight_autocomplete_result_labels(query) {
2520    query = query || '';
2521    if ((!gMatches || !gMatches.length) && (!gGoogleMatches || !gGoogleMatches.length))
2522      return;
2523
2524    var queryLower = query.toLowerCase();
2525    var queryAlnumDot = (queryLower.match(/[\w\.]+/) || [''])[0];
2526    var queryRE = new RegExp(
2527        '(' + queryAlnumDot.replace(/\./g, '\\.') + ')', 'ig');
2528    for (var i=0; i<gMatches.length; i++) {
2529        gMatches[i].__hilabel = gMatches[i].label.replace(
2530            queryRE, '<b>$1</b>');
2531    }
2532    for (var i=0; i<gGoogleMatches.length; i++) {
2533        gGoogleMatches[i].__hilabel = gGoogleMatches[i].label.replace(
2534            queryRE, '<b>$1</b>');
2535    }
2536}
2537
2538function search_focus_changed(obj, focused)
2539{
2540    if (!focused) {
2541        if(obj.value == ""){
2542          $("#search-close").addClass("hide");
2543        }
2544        $(".suggest-card").hide();
2545    }
2546}
2547
2548function submit_search() {
2549  var query = document.getElementById('search_autocomplete').value;
2550  location.hash = 'q=' + query;
2551  loadSearchResults();
2552  $("#searchResults").slideDown('slow', setStickyTop);
2553  return false;
2554}
2555
2556
2557function hideResults() {
2558  $("#searchResults").slideUp('fast', setStickyTop);
2559  $("#search-close").addClass("hide");
2560  location.hash = '';
2561
2562  $("#search_autocomplete").val("").blur();
2563
2564  // reset the ajax search callback to nothing, so results don't appear unless ENTER
2565  searchControl.setSearchStartingCallback(this, function(control, searcher, query) {});
2566
2567  // forcefully regain key-up event control (previously jacked by search api)
2568  $("#search_autocomplete").keyup(function(event) {
2569    return search_changed(event, false, toRoot);
2570  });
2571
2572  return false;
2573}
2574
2575
2576
2577/* ########################################################## */
2578/* ################  CUSTOM SEARCH ENGINE  ################## */
2579/* ########################################################## */
2580
2581var searchControl;
2582google.load('search', '1', {"callback" : function() {
2583            searchControl = new google.search.SearchControl();
2584          } });
2585
2586function loadSearchResults() {
2587  document.getElementById("search_autocomplete").style.color = "#000";
2588
2589  searchControl = new google.search.SearchControl();
2590
2591  // use our existing search form and use tabs when multiple searchers are used
2592  drawOptions = new google.search.DrawOptions();
2593  drawOptions.setDrawMode(google.search.SearchControl.DRAW_MODE_TABBED);
2594  drawOptions.setInput(document.getElementById("search_autocomplete"));
2595
2596  // configure search result options
2597  searchOptions = new google.search.SearcherOptions();
2598  searchOptions.setExpandMode(GSearchControl.EXPAND_MODE_OPEN);
2599
2600  // configure each of the searchers, for each tab
2601  devSiteSearcher = new google.search.WebSearch();
2602  devSiteSearcher.setUserDefinedLabel("All");
2603  devSiteSearcher.setSiteRestriction("001482626316274216503:zu90b7s047u");
2604
2605  designSearcher = new google.search.WebSearch();
2606  designSearcher.setUserDefinedLabel("Design");
2607  designSearcher.setSiteRestriction("http://developer.android.com/design/");
2608
2609  trainingSearcher = new google.search.WebSearch();
2610  trainingSearcher.setUserDefinedLabel("Training");
2611  trainingSearcher.setSiteRestriction("http://developer.android.com/training/");
2612
2613  guidesSearcher = new google.search.WebSearch();
2614  guidesSearcher.setUserDefinedLabel("Guides");
2615  guidesSearcher.setSiteRestriction("http://developer.android.com/guide/");
2616
2617  referenceSearcher = new google.search.WebSearch();
2618  referenceSearcher.setUserDefinedLabel("Reference");
2619  referenceSearcher.setSiteRestriction("http://developer.android.com/reference/");
2620
2621  googleSearcher = new google.search.WebSearch();
2622  googleSearcher.setUserDefinedLabel("Google Services");
2623  googleSearcher.setSiteRestriction("http://developer.android.com/google/");
2624
2625  blogSearcher = new google.search.WebSearch();
2626  blogSearcher.setUserDefinedLabel("Blog");
2627  blogSearcher.setSiteRestriction("http://android-developers.blogspot.com");
2628
2629  // add each searcher to the search control
2630  searchControl.addSearcher(devSiteSearcher, searchOptions);
2631  searchControl.addSearcher(designSearcher, searchOptions);
2632  searchControl.addSearcher(trainingSearcher, searchOptions);
2633  searchControl.addSearcher(guidesSearcher, searchOptions);
2634  searchControl.addSearcher(referenceSearcher, searchOptions);
2635  searchControl.addSearcher(googleSearcher, searchOptions);
2636  searchControl.addSearcher(blogSearcher, searchOptions);
2637
2638  // configure result options
2639  searchControl.setResultSetSize(google.search.Search.LARGE_RESULTSET);
2640  searchControl.setLinkTarget(google.search.Search.LINK_TARGET_SELF);
2641  searchControl.setTimeoutInterval(google.search.SearchControl.TIMEOUT_SHORT);
2642  searchControl.setNoResultsString(google.search.SearchControl.NO_RESULTS_DEFAULT_STRING);
2643
2644  // upon ajax search, refresh the url and search title
2645  searchControl.setSearchStartingCallback(this, function(control, searcher, query) {
2646    updateResultTitle(query);
2647    var query = document.getElementById('search_autocomplete').value;
2648    location.hash = 'q=' + query;
2649  });
2650
2651  // once search results load, set up click listeners
2652  searchControl.setSearchCompleteCallback(this, function(control, searcher, query) {
2653    addResultClickListeners();
2654  });
2655
2656  // draw the search results box
2657  searchControl.draw(document.getElementById("leftSearchControl"), drawOptions);
2658
2659  // get query and execute the search
2660  searchControl.execute(decodeURI(getQuery(location.hash)));
2661
2662  document.getElementById("search_autocomplete").focus();
2663  addTabListeners();
2664}
2665// End of loadSearchResults
2666
2667
2668google.setOnLoadCallback(function(){
2669  if (location.hash.indexOf("q=") == -1) {
2670    // if there's no query in the url, don't search and make sure results are hidden
2671    $('#searchResults').hide();
2672    return;
2673  } else {
2674    // first time loading search results for this page
2675    $('#searchResults').slideDown('slow', setStickyTop);
2676    $("#search-close").removeClass("hide");
2677    loadSearchResults();
2678  }
2679}, true);
2680
2681/* Adjust the scroll position to account for sticky header, only if the hash matches an id.
2682   This does not handle <a name=""> tags. Some CSS fixes those, but only for reference docs. */
2683function offsetScrollForSticky() {
2684  // Ignore if there's no search bar (some special pages have no header)
2685  if ($("#search-container").length < 1) return;
2686
2687  var hash = escape(location.hash.substr(1));
2688  var $matchingElement = $("#"+hash);
2689  // Sanity check that there's an element with that ID on the page
2690  if ($matchingElement.length) {
2691    // If the position of the target element is near the top of the page (<20px, where we expect it
2692    // to be because we need to move it down 60px to become in view), then move it down 60px
2693    if (Math.abs($matchingElement.offset().top - $(window).scrollTop()) < 20) {
2694      $(window).scrollTop($(window).scrollTop() - 60);
2695    }
2696  }
2697}
2698
2699// when an event on the browser history occurs (back, forward, load) requery hash and do search
2700$(window).hashchange( function(){
2701  // Ignore if there's no search bar (some special pages have no header)
2702  if ($("#search-container").length < 1) return;
2703
2704  // If the hash isn't a search query or there's an error in the query,
2705  // then adjust the scroll position to account for sticky header, then exit.
2706  if ((location.hash.indexOf("q=") == -1) || (query == "undefined")) {
2707    // If the results pane is open, close it.
2708    if (!$("#searchResults").is(":hidden")) {
2709      hideResults();
2710    }
2711    offsetScrollForSticky();
2712    return;
2713  }
2714
2715  // Otherwise, we have a search to do
2716  var query = decodeURI(getQuery(location.hash));
2717  searchControl.execute(query);
2718  $('#searchResults').slideDown('slow', setStickyTop);
2719  $("#search_autocomplete").focus();
2720  $("#search-close").removeClass("hide");
2721
2722  updateResultTitle(query);
2723});
2724
2725function updateResultTitle(query) {
2726  $("#searchTitle").html("Results for <em>" + escapeHTML(query) + "</em>");
2727}
2728
2729// forcefully regain key-up event control (previously jacked by search api)
2730$("#search_autocomplete").keyup(function(event) {
2731  return search_changed(event, false, toRoot);
2732});
2733
2734// add event listeners to each tab so we can track the browser history
2735function addTabListeners() {
2736  var tabHeaders = $(".gsc-tabHeader");
2737  for (var i = 0; i < tabHeaders.length; i++) {
2738    $(tabHeaders[i]).attr("id",i).click(function() {
2739    /*
2740      // make a copy of the page numbers for the search left pane
2741      setTimeout(function() {
2742        // remove any residual page numbers
2743        $('#searchResults .gsc-tabsArea .gsc-cursor-box.gs-bidi-start-align').remove();
2744        // move the page numbers to the left position; make a clone,
2745        // because the element is drawn to the DOM only once
2746        // and because we're going to remove it (previous line),
2747        // we need it to be available to move again as the user navigates
2748        $('#searchResults .gsc-webResult .gsc-cursor-box.gs-bidi-start-align:visible')
2749                        .clone().appendTo('#searchResults .gsc-tabsArea');
2750        }, 200);
2751      */
2752    });
2753  }
2754  setTimeout(function(){$(tabHeaders[0]).click()},200);
2755}
2756
2757// add analytics tracking events to each result link
2758function addResultClickListeners() {
2759  $("#searchResults a.gs-title").each(function(index, link) {
2760    // When user clicks enter for Google search results, track it
2761    $(link).click(function() {
2762      ga('send', 'event', 'Google Click', 'clicked: ' + $(this).attr('href'),
2763                'query: ' + $("#search_autocomplete").val().toLowerCase());
2764    });
2765  });
2766}
2767
2768
2769function getQuery(hash) {
2770  var queryParts = hash.split('=');
2771  return queryParts[1];
2772}
2773
2774/* returns the given string with all HTML brackets converted to entities
2775    TODO: move this to the site's JS library */
2776function escapeHTML(string) {
2777  return string.replace(/</g,"&lt;")
2778                .replace(/>/g,"&gt;");
2779}
2780
2781
2782
2783
2784
2785
2786
2787/* ######################################################## */
2788/* #################  JAVADOC REFERENCE ################### */
2789/* ######################################################## */
2790
2791/* Initialize some droiddoc stuff, but only if we're in the reference */
2792if (location.pathname.indexOf("/reference") == 0) {
2793  if(!(location.pathname.indexOf("/reference-gms/packages.html") == 0)
2794    && !(location.pathname.indexOf("/reference-gcm/packages.html") == 0)
2795    && !(location.pathname.indexOf("/reference/com/google") == 0)) {
2796    $(document).ready(function() {
2797      // init available apis based on user pref
2798      changeApiLevel();
2799      initSidenavHeightResize()
2800      });
2801  }
2802}
2803
2804var API_LEVEL_COOKIE = "api_level";
2805var minLevel = 1;
2806var maxLevel = 1;
2807
2808/******* SIDENAV DIMENSIONS ************/
2809
2810  function initSidenavHeightResize() {
2811    // Change the drag bar size to nicely fit the scrollbar positions
2812    var $dragBar = $(".ui-resizable-s");
2813    $dragBar.css({'width': $dragBar.parent().width() - 5 + "px"});
2814
2815    $( "#resize-packages-nav" ).resizable({
2816      containment: "#nav-panels",
2817      handles: "s",
2818      alsoResize: "#packages-nav",
2819      resize: function(event, ui) { resizeNav(); }, /* resize the nav while dragging */
2820      stop: function(event, ui) { saveNavPanels(); } /* once stopped, save the sizes to cookie  */
2821      });
2822
2823  }
2824
2825function updateSidenavFixedWidth() {
2826  if (!sticky) return;
2827  $('#devdoc-nav').css({
2828    'width' : $('#side-nav').css('width'),
2829    'margin' : $('#side-nav').css('margin')
2830  });
2831  $('#devdoc-nav a.totop').css({'display':'block','width':$("#nav").innerWidth()+'px'});
2832
2833  initSidenavHeightResize();
2834}
2835
2836function updateSidenavFullscreenWidth() {
2837  if (!sticky) return;
2838  $('#devdoc-nav').css({
2839    'width' : $('#side-nav').css('width'),
2840    'margin' : $('#side-nav').css('margin')
2841  });
2842  $('#devdoc-nav .totop').css({'left': 'inherit'});
2843
2844  initSidenavHeightResize();
2845}
2846
2847function buildApiLevelSelector() {
2848  maxLevel = SINCE_DATA.length;
2849  var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE));
2850  userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default
2851
2852  minLevel = parseInt($("#doc-api-level").attr("class"));
2853  // Handle provisional api levels; the provisional level will always be the highest possible level
2854  // Provisional api levels will also have a length; other stuff that's just missing a level won't,
2855  // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class)
2856  if (isNaN(minLevel) && minLevel.length) {
2857    minLevel = maxLevel;
2858  }
2859  var select = $("#apiLevelSelector").html("").change(changeApiLevel);
2860  for (var i = maxLevel-1; i >= 0; i--) {
2861    var option = $("<option />").attr("value",""+SINCE_DATA[i]).append(""+SINCE_DATA[i]);
2862  //  if (SINCE_DATA[i] < minLevel) option.addClass("absent"); // always false for strings (codenames)
2863    select.append(option);
2864  }
2865
2866  // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true)
2867  var selectedLevelItem = $("#apiLevelSelector option[value='"+userApiLevel+"']").get(0);
2868  selectedLevelItem.setAttribute('selected',true);
2869}
2870
2871function changeApiLevel() {
2872  maxLevel = SINCE_DATA.length;
2873  var selectedLevel = maxLevel;
2874
2875  selectedLevel = parseInt($("#apiLevelSelector option:selected").val());
2876  toggleVisisbleApis(selectedLevel, "body");
2877
2878  writeCookie(API_LEVEL_COOKIE, selectedLevel, null);
2879
2880  if (selectedLevel < minLevel) {
2881    var thing = ($("#jd-header").html().indexOf("package") != -1) ? "package" : "class";
2882    $("#naMessage").show().html("<div><p><strong>This " + thing
2883              + " requires API level " + minLevel + " or higher.</strong></p>"
2884              + "<p>This document is hidden because your selected API level for the documentation is "
2885              + selectedLevel + ". You can change the documentation API level with the selector "
2886              + "above the left navigation.</p>"
2887              + "<p>For more information about specifying the API level your app requires, "
2888              + "read <a href='" + toRoot + "training/basics/supporting-devices/platforms.html'"
2889              + ">Supporting Different Platform Versions</a>.</p>"
2890              + "<input type='button' value='OK, make this page visible' "
2891              + "title='Change the API level to " + minLevel + "' "
2892              + "onclick='$(\"#apiLevelSelector\").val(\"" + minLevel + "\");changeApiLevel();' />"
2893              + "</div>");
2894  } else {
2895    $("#naMessage").hide();
2896  }
2897}
2898
2899function toggleVisisbleApis(selectedLevel, context) {
2900  var apis = $(".api",context);
2901  apis.each(function(i) {
2902    var obj = $(this);
2903    var className = obj.attr("class");
2904    var apiLevelIndex = className.lastIndexOf("-")+1;
2905    var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex);
2906    apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length;
2907    var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex);
2908    if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail
2909      return;
2910    }
2911    apiLevel = parseInt(apiLevel);
2912
2913    // Handle provisional api levels; if this item's level is the provisional one, set it to the max
2914    var selectedLevelNum = parseInt(selectedLevel)
2915    var apiLevelNum = parseInt(apiLevel);
2916    if (isNaN(apiLevelNum)) {
2917        apiLevelNum = maxLevel;
2918    }
2919
2920    // Grey things out that aren't available and give a tooltip title
2921    if (apiLevelNum > selectedLevelNum) {
2922      obj.addClass("absent").attr("title","Requires API Level \""
2923            + apiLevel + "\" or higher. To reveal, change the target API level "
2924              + "above the left navigation.");
2925    }
2926    else obj.removeClass("absent").removeAttr("title");
2927  });
2928}
2929
2930
2931
2932
2933/* #################  SIDENAV TREE VIEW ################### */
2934
2935function new_node(me, mom, text, link, children_data, api_level)
2936{
2937  var node = new Object();
2938  node.children = Array();
2939  node.children_data = children_data;
2940  node.depth = mom.depth + 1;
2941
2942  node.li = document.createElement("li");
2943  mom.get_children_ul().appendChild(node.li);
2944
2945  node.label_div = document.createElement("div");
2946  node.label_div.className = "label";
2947  if (api_level != null) {
2948    $(node.label_div).addClass("api");
2949    $(node.label_div).addClass("api-level-"+api_level);
2950  }
2951  node.li.appendChild(node.label_div);
2952
2953  if (children_data != null) {
2954    node.expand_toggle = document.createElement("a");
2955    node.expand_toggle.href = "javascript:void(0)";
2956    node.expand_toggle.onclick = function() {
2957          if (node.expanded) {
2958            $(node.get_children_ul()).slideUp("fast");
2959            node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png";
2960            node.expanded = false;
2961          } else {
2962            expand_node(me, node);
2963          }
2964       };
2965    node.label_div.appendChild(node.expand_toggle);
2966
2967    node.plus_img = document.createElement("img");
2968    node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png";
2969    node.plus_img.className = "plus";
2970    node.plus_img.width = "8";
2971    node.plus_img.border = "0";
2972    node.expand_toggle.appendChild(node.plus_img);
2973
2974    node.expanded = false;
2975  }
2976
2977  var a = document.createElement("a");
2978  node.label_div.appendChild(a);
2979  node.label = document.createTextNode(text);
2980  a.appendChild(node.label);
2981  if (link) {
2982    a.href = me.toroot + link;
2983  } else {
2984    if (children_data != null) {
2985      a.className = "nolink";
2986      a.href = "javascript:void(0)";
2987      a.onclick = node.expand_toggle.onclick;
2988      // This next line shouldn't be necessary.  I'll buy a beer for the first
2989      // person who figures out how to remove this line and have the link
2990      // toggle shut on the first try. --joeo@android.com
2991      node.expanded = false;
2992    }
2993  }
2994
2995
2996  node.children_ul = null;
2997  node.get_children_ul = function() {
2998      if (!node.children_ul) {
2999        node.children_ul = document.createElement("ul");
3000        node.children_ul.className = "children_ul";
3001        node.children_ul.style.display = "none";
3002        node.li.appendChild(node.children_ul);
3003      }
3004      return node.children_ul;
3005    };
3006
3007  return node;
3008}
3009
3010
3011
3012
3013function expand_node(me, node)
3014{
3015  if (node.children_data && !node.expanded) {
3016    if (node.children_visited) {
3017      $(node.get_children_ul()).slideDown("fast");
3018    } else {
3019      get_node(me, node);
3020      if ($(node.label_div).hasClass("absent")) {
3021        $(node.get_children_ul()).addClass("absent");
3022      }
3023      $(node.get_children_ul()).slideDown("fast");
3024    }
3025    node.plus_img.src = me.toroot + "assets/images/triangle-opened-small.png";
3026    node.expanded = true;
3027
3028    // perform api level toggling because new nodes are new to the DOM
3029    var selectedLevel = $("#apiLevelSelector option:selected").val();
3030    toggleVisisbleApis(selectedLevel, "#side-nav");
3031  }
3032}
3033
3034function get_node(me, mom)
3035{
3036  mom.children_visited = true;
3037  for (var i in mom.children_data) {
3038    var node_data = mom.children_data[i];
3039    mom.children[i] = new_node(me, mom, node_data[0], node_data[1],
3040        node_data[2], node_data[3]);
3041  }
3042}
3043
3044function this_page_relative(toroot)
3045{
3046  var full = document.location.pathname;
3047  var file = "";
3048  if (toroot.substr(0, 1) == "/") {
3049    if (full.substr(0, toroot.length) == toroot) {
3050      return full.substr(toroot.length);
3051    } else {
3052      // the file isn't under toroot.  Fail.
3053      return null;
3054    }
3055  } else {
3056    if (toroot != "./") {
3057      toroot = "./" + toroot;
3058    }
3059    do {
3060      if (toroot.substr(toroot.length-3, 3) == "../" || toroot == "./") {
3061        var pos = full.lastIndexOf("/");
3062        file = full.substr(pos) + file;
3063        full = full.substr(0, pos);
3064        toroot = toroot.substr(0, toroot.length-3);
3065      }
3066    } while (toroot != "" && toroot != "/");
3067    return file.substr(1);
3068  }
3069}
3070
3071function find_page(url, data)
3072{
3073  var nodes = data;
3074  var result = null;
3075  for (var i in nodes) {
3076    var d = nodes[i];
3077    if (d[1] == url) {
3078      return new Array(i);
3079    }
3080    else if (d[2] != null) {
3081      result = find_page(url, d[2]);
3082      if (result != null) {
3083        return (new Array(i).concat(result));
3084      }
3085    }
3086  }
3087  return null;
3088}
3089
3090function init_default_navtree(toroot) {
3091  // load json file for navtree data
3092  $.getScript(toRoot + 'navtree_data.js', function(data, textStatus, jqxhr) {
3093      // when the file is loaded, initialize the tree
3094      if(jqxhr.status === 200) {
3095          init_navtree("tree-list", toroot, NAVTREE_DATA);
3096      }
3097  });
3098
3099  // perform api level toggling because because the whole tree is new to the DOM
3100  var selectedLevel = $("#apiLevelSelector option:selected").val();
3101  toggleVisisbleApis(selectedLevel, "#side-nav");
3102}
3103
3104function init_navtree(navtree_id, toroot, root_nodes)
3105{
3106  var me = new Object();
3107  me.toroot = toroot;
3108  me.node = new Object();
3109
3110  me.node.li = document.getElementById(navtree_id);
3111  me.node.children_data = root_nodes;
3112  me.node.children = new Array();
3113  me.node.children_ul = document.createElement("ul");
3114  me.node.get_children_ul = function() { return me.node.children_ul; };
3115  //me.node.children_ul.className = "children_ul";
3116  me.node.li.appendChild(me.node.children_ul);
3117  me.node.depth = 0;
3118
3119  get_node(me, me.node);
3120
3121  me.this_page = this_page_relative(toroot);
3122  me.breadcrumbs = find_page(me.this_page, root_nodes);
3123  if (me.breadcrumbs != null && me.breadcrumbs.length != 0) {
3124    var mom = me.node;
3125    for (var i in me.breadcrumbs) {
3126      var j = me.breadcrumbs[i];
3127      mom = mom.children[j];
3128      expand_node(me, mom);
3129    }
3130    mom.label_div.className = mom.label_div.className + " selected";
3131    addLoadEvent(function() {
3132      scrollIntoView("nav-tree");
3133      });
3134  }
3135}
3136
3137
3138
3139
3140
3141
3142
3143
3144/* TODO: eliminate redundancy with non-google functions */
3145function init_google_navtree(navtree_id, toroot, root_nodes)
3146{
3147  var me = new Object();
3148  me.toroot = toroot;
3149  me.node = new Object();
3150
3151  me.node.li = document.getElementById(navtree_id);
3152  if (!me.node.li) {
3153    return;
3154  }
3155
3156  me.node.children_data = root_nodes;
3157  me.node.children = new Array();
3158  me.node.children_ul = document.createElement("ul");
3159  me.node.get_children_ul = function() { return me.node.children_ul; };
3160  //me.node.children_ul.className = "children_ul";
3161  me.node.li.appendChild(me.node.children_ul);
3162  me.node.depth = 0;
3163
3164  get_google_node(me, me.node);
3165}
3166
3167function new_google_node(me, mom, text, link, children_data, api_level)
3168{
3169  var node = new Object();
3170  var child;
3171  node.children = Array();
3172  node.children_data = children_data;
3173  node.depth = mom.depth + 1;
3174  node.get_children_ul = function() {
3175      if (!node.children_ul) {
3176        node.children_ul = document.createElement("ul");
3177        node.children_ul.className = "tree-list-children";
3178        node.li.appendChild(node.children_ul);
3179      }
3180      return node.children_ul;
3181    };
3182  node.li = document.createElement("li");
3183
3184  mom.get_children_ul().appendChild(node.li);
3185
3186
3187  if(link) {
3188    child = document.createElement("a");
3189
3190  }
3191  else {
3192    child = document.createElement("span");
3193    child.className = "tree-list-subtitle";
3194
3195  }
3196  if (children_data != null) {
3197    node.li.className="nav-section";
3198    node.label_div = document.createElement("div");
3199    node.label_div.className = "nav-section-header-ref";
3200    node.li.appendChild(node.label_div);
3201    get_google_node(me, node);
3202    node.label_div.appendChild(child);
3203  }
3204  else {
3205    node.li.appendChild(child);
3206  }
3207  if(link) {
3208    child.href = me.toroot + link;
3209  }
3210  node.label = document.createTextNode(text);
3211  child.appendChild(node.label);
3212
3213  node.children_ul = null;
3214
3215  return node;
3216}
3217
3218function get_google_node(me, mom)
3219{
3220  mom.children_visited = true;
3221  var linkText;
3222  for (var i in mom.children_data) {
3223    var node_data = mom.children_data[i];
3224    linkText = node_data[0];
3225
3226    if(linkText.match("^"+"com.google.android")=="com.google.android"){
3227      linkText = linkText.substr(19, linkText.length);
3228    }
3229      mom.children[i] = new_google_node(me, mom, linkText, node_data[1],
3230          node_data[2], node_data[3]);
3231  }
3232}
3233
3234
3235
3236
3237
3238
3239/****** NEW version of script to build google and sample navs dynamically ******/
3240// TODO: update Google reference docs to tolerate this new implementation
3241
3242var NODE_NAME = 0;
3243var NODE_HREF = 1;
3244var NODE_GROUP = 2;
3245var NODE_TAGS = 3;
3246var NODE_CHILDREN = 4;
3247
3248function init_google_navtree2(navtree_id, data)
3249{
3250  var $containerUl = $("#"+navtree_id);
3251  for (var i in data) {
3252    var node_data = data[i];
3253    $containerUl.append(new_google_node2(node_data));
3254  }
3255
3256  // Make all third-generation list items 'sticky' to prevent them from collapsing
3257  $containerUl.find('li li li.nav-section').addClass('sticky');
3258
3259  initExpandableNavItems("#"+navtree_id);
3260}
3261
3262function new_google_node2(node_data)
3263{
3264  var linkText = node_data[NODE_NAME];
3265  if(linkText.match("^"+"com.google.android")=="com.google.android"){
3266    linkText = linkText.substr(19, linkText.length);
3267  }
3268  var $li = $('<li>');
3269  var $a;
3270  if (node_data[NODE_HREF] != null) {
3271    $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >'
3272        + linkText + '</a>');
3273  } else {
3274    $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >'
3275        + linkText + '/</a>');
3276  }
3277  var $childUl = $('<ul>');
3278  if (node_data[NODE_CHILDREN] != null) {
3279    $li.addClass("nav-section");
3280    $a = $('<div class="nav-section-header">').append($a);
3281    if (node_data[NODE_HREF] == null) $a.addClass('empty');
3282
3283    for (var i in node_data[NODE_CHILDREN]) {
3284      var child_node_data = node_data[NODE_CHILDREN][i];
3285      $childUl.append(new_google_node2(child_node_data));
3286    }
3287    $li.append($childUl);
3288  }
3289  $li.prepend($a);
3290
3291  return $li;
3292}
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304function showGoogleRefTree() {
3305  init_default_google_navtree(toRoot);
3306  init_default_gcm_navtree(toRoot);
3307}
3308
3309function init_default_google_navtree(toroot) {
3310  // load json file for navtree data
3311  $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) {
3312      // when the file is loaded, initialize the tree
3313      if(jqxhr.status === 200) {
3314          init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA);
3315          highlightSidenav();
3316          resizeNav();
3317      }
3318  });
3319}
3320
3321function init_default_gcm_navtree(toroot) {
3322  // load json file for navtree data
3323  $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) {
3324      // when the file is loaded, initialize the tree
3325      if(jqxhr.status === 200) {
3326          init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA);
3327          highlightSidenav();
3328          resizeNav();
3329      }
3330  });
3331}
3332
3333function showSamplesRefTree() {
3334  init_default_samples_navtree(toRoot);
3335}
3336
3337function init_default_samples_navtree(toroot) {
3338  // load json file for navtree data
3339  $.getScript(toRoot + 'samples_navtree_data.js', function(data, textStatus, jqxhr) {
3340      // when the file is loaded, initialize the tree
3341      if(jqxhr.status === 200) {
3342          // hack to remove the "about the samples" link then put it back in
3343          // after we nuke the list to remove the dummy static list of samples
3344          var $firstLi = $("#nav.samples-nav > li:first-child").clone();
3345          $("#nav.samples-nav").empty();
3346          $("#nav.samples-nav").append($firstLi);
3347
3348          init_google_navtree2("nav.samples-nav", SAMPLES_NAVTREE_DATA);
3349          highlightSidenav();
3350          resizeNav();
3351          if ($("#jd-content #samples").length) {
3352            showSamples();
3353          }
3354      }
3355  });
3356}
3357
3358/* TOGGLE INHERITED MEMBERS */
3359
3360/* Toggle an inherited class (arrow toggle)
3361 * @param linkObj  The link that was clicked.
3362 * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
3363 *                'null' to simply toggle.
3364 */
3365function toggleInherited(linkObj, expand) {
3366    var base = linkObj.getAttribute("id");
3367    var list = document.getElementById(base + "-list");
3368    var summary = document.getElementById(base + "-summary");
3369    var trigger = document.getElementById(base + "-trigger");
3370    var a = $(linkObj);
3371    if ( (expand == null && a.hasClass("closed")) || expand ) {
3372        list.style.display = "none";
3373        summary.style.display = "block";
3374        trigger.src = toRoot + "assets/images/triangle-opened.png";
3375        a.removeClass("closed");
3376        a.addClass("opened");
3377    } else if ( (expand == null && a.hasClass("opened")) || (expand == false) ) {
3378        list.style.display = "block";
3379        summary.style.display = "none";
3380        trigger.src = toRoot + "assets/images/triangle-closed.png";
3381        a.removeClass("opened");
3382        a.addClass("closed");
3383    }
3384    return false;
3385}
3386
3387/* Toggle all inherited classes in a single table (e.g. all inherited methods)
3388 * @param linkObj  The link that was clicked.
3389 * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
3390 *                'null' to simply toggle.
3391 */
3392function toggleAllInherited(linkObj, expand) {
3393  var a = $(linkObj);
3394  var table = $(a.parent().parent().parent()); // ugly way to get table/tbody
3395  var expandos = $(".jd-expando-trigger", table);
3396  if ( (expand == null && a.text() == "[Expand]") || expand ) {
3397    expandos.each(function(i) {
3398      toggleInherited(this, true);
3399    });
3400    a.text("[Collapse]");
3401  } else if ( (expand == null && a.text() == "[Collapse]") || (expand == false) ) {
3402    expandos.each(function(i) {
3403      toggleInherited(this, false);
3404    });
3405    a.text("[Expand]");
3406  }
3407  return false;
3408}
3409
3410/* Toggle all inherited members in the class (link in the class title)
3411 */
3412function toggleAllClassInherited() {
3413  var a = $("#toggleAllClassInherited"); // get toggle link from class title
3414  var toggles = $(".toggle-all", $("#body-content"));
3415  if (a.text() == "[Expand All]") {
3416    toggles.each(function(i) {
3417      toggleAllInherited(this, true);
3418    });
3419    a.text("[Collapse All]");
3420  } else {
3421    toggles.each(function(i) {
3422      toggleAllInherited(this, false);
3423    });
3424    a.text("[Expand All]");
3425  }
3426  return false;
3427}
3428
3429/* Expand all inherited members in the class. Used when initiating page search */
3430function ensureAllInheritedExpanded() {
3431  var toggles = $(".toggle-all", $("#body-content"));
3432  toggles.each(function(i) {
3433    toggleAllInherited(this, true);
3434  });
3435  $("#toggleAllClassInherited").text("[Collapse All]");
3436}
3437
3438
3439/* HANDLE KEY EVENTS
3440 * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search)
3441 */
3442var agent = navigator['userAgent'].toLowerCase();
3443var mac = agent.indexOf("macintosh") != -1;
3444
3445$(document).keydown( function(e) {
3446var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key
3447  if (control && e.which == 70) {  // 70 is "F"
3448    ensureAllInheritedExpanded();
3449  }
3450});
3451
3452
3453
3454
3455
3456
3457/* On-demand functions */
3458
3459/** Move sample code line numbers out of PRE block and into non-copyable column */
3460function initCodeLineNumbers() {
3461  var numbers = $("#codesample-block a.number");
3462  if (numbers.length) {
3463    $("#codesample-line-numbers").removeClass("hidden").append(numbers);
3464  }
3465
3466  $(document).ready(function() {
3467    // select entire line when clicked
3468    $("span.code-line").click(function() {
3469      if (!shifted) {
3470        selectText(this);
3471      }
3472    });
3473    // invoke line link on double click
3474    $(".code-line").dblclick(function() {
3475      document.location.hash = $(this).attr('id');
3476    });
3477    // highlight the line when hovering on the number
3478    $("#codesample-line-numbers a.number").mouseover(function() {
3479      var id = $(this).attr('href');
3480      $(id).css('background','#e7e7e7');
3481    });
3482    $("#codesample-line-numbers a.number").mouseout(function() {
3483      var id = $(this).attr('href');
3484      $(id).css('background','none');
3485    });
3486  });
3487}
3488
3489// create SHIFT key binder to avoid the selectText method when selecting multiple lines
3490var shifted = false;
3491$(document).bind('keyup keydown', function(e){shifted = e.shiftKey; return true;} );
3492
3493// courtesy of jasonedelman.com
3494function selectText(element) {
3495    var doc = document
3496        , range, selection
3497    ;
3498    if (doc.body.createTextRange) { //ms
3499        range = doc.body.createTextRange();
3500        range.moveToElementText(element);
3501        range.select();
3502    } else if (window.getSelection) { //all others
3503        selection = window.getSelection();
3504        range = doc.createRange();
3505        range.selectNodeContents(element);
3506        selection.removeAllRanges();
3507        selection.addRange(range);
3508    }
3509}
3510
3511
3512
3513
3514/** Display links and other information about samples that match the
3515    group specified by the URL */
3516function showSamples() {
3517  var group = $("#samples").attr('class');
3518  $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>");
3519
3520  var $ul = $("<ul>");
3521  $selectedLi = $("#nav li.selected");
3522
3523  $selectedLi.children("ul").children("li").each(function() {
3524      var $li = $("<li>").append($(this).find("a").first().clone());
3525      $ul.append($li);
3526  });
3527
3528  $("#samples").append($ul);
3529
3530}
3531
3532
3533
3534/* ########################################################## */
3535/* ###################  RESOURCE CARDS  ##################### */
3536/* ########################################################## */
3537
3538/** Handle resource queries, collections, and grids (sections). Requires
3539    jd_tag_helpers.js and the *_unified_data.js to be loaded. */
3540
3541(function() {
3542  // Prevent the same resource from being loaded more than once per page.
3543  var addedPageResources = {};
3544
3545  $(document).ready(function() {
3546    // Need to initialize hero carousel before other sections for dedupe
3547    // to work correctly.
3548    $('[data-carousel-query]').dacCarouselQuery();
3549
3550    $('.resource-widget').each(function() {
3551      initResourceWidget(this);
3552    });
3553
3554    /* Pass the line height to ellipsisfade() to adjust the height of the
3555    text container to show the max number of lines possible, without
3556    showing lines that are cut off. This works with the css ellipsis
3557    classes to fade last text line and apply an ellipsis char. */
3558
3559    //card text currently uses 20px line height.
3560    var lineHeight = 20;
3561    $('.card-info .text').ellipsisfade(lineHeight);
3562  });
3563
3564  /*
3565    Three types of resource layouts:
3566    Flow - Uses a fixed row-height flow using float left style.
3567    Carousel - Single card slideshow all same dimension absolute.
3568    Stack - Uses fixed columns and flexible element height.
3569  */
3570  function initResourceWidget(widget) {
3571    var $widget = $(widget);
3572    var isFlow = $widget.hasClass('resource-flow-layout'),
3573        isCarousel = $widget.hasClass('resource-carousel-layout'),
3574        isStack = $widget.hasClass('resource-stack-layout');
3575
3576    // remove illegal col-x class which is not relevant anymore thanks to responsive styles.
3577    var m = $widget.get(0).className.match(/\bcol-(\d+)\b/);
3578    if (m && !$widget.is('.cols > *')) {
3579      $widget.removeClass('col-' + m[1]);
3580    }
3581
3582    var opts = {
3583      cardSizes: ($widget.data('cardsizes') || '').split(','),
3584      maxResults: parseInt($widget.data('maxresults') || '100', 10),
3585      initialResults: $widget.data('initialResults'),
3586      itemsPerPage: $widget.data('itemsperpage'),
3587      sortOrder: $widget.data('sortorder'),
3588      query: $widget.data('query'),
3589      section: $widget.data('section'),
3590      /* Added by LFL 6/6/14 */
3591      resourceStyle: $widget.data('resourcestyle') || 'card',
3592      stackSort: $widget.data('stacksort') || 'true'
3593    };
3594
3595    // run the search for the set of resources to show
3596
3597    var resources = buildResourceList(opts);
3598
3599    if (isFlow) {
3600      drawResourcesFlowWidget($widget, opts, resources);
3601    } else if (isCarousel) {
3602      drawResourcesCarouselWidget($widget, opts, resources);
3603    } else if (isStack) {
3604      /* Looks like this got removed and is not used, so repurposing for the
3605          homepage style layout.
3606          Modified by LFL 6/6/14
3607      */
3608      //var sections = buildSectionList(opts);
3609      opts['numStacks'] = $widget.data('numstacks');
3610      drawResourcesStackWidget($widget, opts, resources/*, sections*/);
3611    }
3612  }
3613
3614  /* Initializes a Resource Carousel Widget */
3615  function drawResourcesCarouselWidget($widget, opts, resources) {
3616    $widget.empty();
3617    var plusone = false; // stop showing plusone buttons on cards
3618
3619    $widget.addClass('resource-card slideshow-container')
3620      .append($('<a>').addClass('slideshow-prev').text('Prev'))
3621      .append($('<a>').addClass('slideshow-next').text('Next'));
3622
3623    var css = { 'width': $widget.width() + 'px',
3624                'height': $widget.height() + 'px' };
3625
3626    var $ul = $('<ul>');
3627
3628    for (var i = 0; i < resources.length; ++i) {
3629      var $card = $('<a>')
3630        .attr('href', cleanUrl(resources[i].url))
3631        .decorateResourceCard(resources[i],plusone);
3632
3633      $('<li>').css(css)
3634          .append($card)
3635          .appendTo($ul);
3636    }
3637
3638    $('<div>').addClass('frame')
3639      .append($ul)
3640      .appendTo($widget);
3641
3642    $widget.dacSlideshow({
3643      auto: true,
3644      btnPrev: '.slideshow-prev',
3645      btnNext: '.slideshow-next'
3646    });
3647  };
3648
3649  /* Initializes a Resource Card Stack Widget (column-based layout)
3650     Modified by LFL 6/6/14
3651   */
3652  function drawResourcesStackWidget($widget, opts, resources, sections) {
3653    // Don't empty widget, grab all items inside since they will be the first
3654    // items stacked, followed by the resource query
3655    var plusone = false; // stop showing plusone buttons on cards
3656    var cards = $widget.find('.resource-card').detach().toArray();
3657    var numStacks = opts.numStacks || 1;
3658    var $stacks = [];
3659    var urlString;
3660
3661    for (var i = 0; i < numStacks; ++i) {
3662      $stacks[i] = $('<div>').addClass('resource-card-stack')
3663          .appendTo($widget);
3664    }
3665
3666    var sectionResources = [];
3667
3668    // Extract any subsections that are actually resource cards
3669    if (sections) {
3670      for (var i = 0; i < sections.length; ++i) {
3671        if (!sections[i].sections || !sections[i].sections.length) {
3672          // Render it as a resource card
3673          sectionResources.push(
3674            $('<a>')
3675              .addClass('resource-card section-card')
3676              .attr('href', cleanUrl(sections[i].resource.url))
3677              .decorateResourceCard(sections[i].resource,plusone)[0]
3678          );
3679
3680        } else {
3681          cards.push(
3682            $('<div>')
3683              .addClass('resource-card section-card-menu')
3684              .decorateResourceSection(sections[i],plusone)[0]
3685          );
3686        }
3687      }
3688    }
3689
3690    cards = cards.concat(sectionResources);
3691
3692    for (var i = 0; i < resources.length; ++i) {
3693      var $card = createResourceElement(resources[i], opts);
3694
3695      if (opts.resourceStyle.indexOf('related') > -1) {
3696        $card.addClass('related-card');
3697      }
3698
3699      cards.push($card[0]);
3700    }
3701
3702    if (opts.stackSort != 'false') {
3703      for (var i = 0; i < cards.length; ++i) {
3704        // Find the stack with the shortest height, but give preference to
3705        // left to right order.
3706        var minHeight = $stacks[0].height();
3707        var minIndex = 0;
3708
3709        for (var j = 1; j < numStacks; ++j) {
3710          var height = $stacks[j].height();
3711          if (height < minHeight - 45) {
3712            minHeight = height;
3713            minIndex = j;
3714          }
3715        }
3716
3717        $stacks[minIndex].append($(cards[i]));
3718      }
3719    }
3720
3721  };
3722
3723  /*
3724    Create a resource card using the given resource object and a list of html
3725     configured options. Returns a jquery object containing the element.
3726  */
3727  function createResourceElement(resource, opts, plusone) {
3728    var $el;
3729
3730    // The difference here is that generic cards are not entirely clickable
3731    // so its a div instead of an a tag, also the generic one is not given
3732    // the resource-card class so it appears with a transparent background
3733    // and can be styled in whatever way the css setup.
3734    if (opts.resourceStyle == 'generic') {
3735      $el = $('<div>')
3736        .addClass('resource')
3737        .attr('href', cleanUrl(resource.url))
3738        .decorateResource(resource, opts);
3739    } else {
3740      var cls = 'resource resource-card';
3741
3742      $el = $('<a>')
3743        .addClass(cls)
3744        .attr('href', cleanUrl(resource.url))
3745        .decorateResourceCard(resource, plusone);
3746    }
3747
3748    return $el;
3749  }
3750
3751  function createResponsiveFlowColumn(cardSize) {
3752    var cardWidth = parseInt(cardSize.match(/(\d+)/)[1], 10);
3753    var column = $('<div>').addClass('col-' + (cardWidth / 3) + 'of6');
3754    if (cardWidth < 9) {
3755      column.addClass('col-tablet-1of2');
3756    } else if (cardWidth > 9 && cardWidth < 18) {
3757      column.addClass('col-tablet-1of1');
3758    }
3759    if (cardWidth < 18) {
3760      column.addClass('col-mobile-1of1')
3761    }
3762    return column;
3763  }
3764
3765  /* Initializes a flow widget, see distribute.scss for generating accompanying css */
3766  function drawResourcesFlowWidget($widget, opts, resources) {
3767    $widget.empty().addClass('cols');
3768    var cardSizes = opts.cardSizes || ['6x6'];
3769    var initialResults = opts.initialResults || resources.length;
3770    var i = 0, j = 0;
3771    var plusone = false; // stop showing plusone buttons on cards
3772    var cardParent = $widget;
3773
3774    while (i < resources.length) {
3775
3776      if (i === initialResults && initialResults < resources.length) {
3777        // Toggle remaining cards
3778        cardParent = $('<div class="dac-toggle-content clearfix">').appendTo($widget);
3779        $widget.addClass('dac-toggle');
3780        $('<div class="col-1of1 dac-section-links dac-text-center">')
3781          .append(
3782            $('<div class="dac-section-link" data-toggle="section">')
3783              .append('<span class="dac-toggle-expand">More<i class="dac-sprite dac-auto-unfold-more"></i></span>')
3784              .append('<span class="dac-toggle-collapse">Less<i class="dac-sprite dac-auto-unfold-less"></i></span>')
3785          )
3786          .appendTo($widget)
3787      }
3788
3789      var cardSize = cardSizes[j++ % cardSizes.length];
3790      cardSize = cardSize.replace(/^\s+|\s+$/,'');
3791
3792      var column = createResponsiveFlowColumn(cardSize).appendTo(cardParent);
3793
3794      // A stack has a third dimension which is the number of stacked items
3795      var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/);
3796      var stackCount = 0;
3797      var $stackDiv = null;
3798
3799      if (isStack) {
3800        // Create a stack container which should have the dimensions defined
3801        // by the product of the items inside.
3802        $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1]
3803            + 'x' + isStack[2] * isStack[3]) .appendTo(column);
3804      }
3805
3806      // Build each stack item or just a single item
3807      do {
3808        var resource = resources[i];
3809
3810        var $card = createResourceElement(resources[i], opts, plusone);
3811
3812        $card.addClass('resource-card-' + cardSize +
3813          ' resource-card-' + resource.type);
3814
3815        if (isStack) {
3816          $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]);
3817          if (++stackCount == parseInt(isStack[3])) {
3818            $card.addClass('resource-card-row-stack-last');
3819            stackCount = 0;
3820          }
3821        } else {
3822          stackCount = 0;
3823        }
3824
3825        $card.appendTo($stackDiv || column);
3826
3827      } while (++i < resources.length && stackCount > 0);
3828    }
3829  }
3830
3831  /* Build a site map of resources using a section as a root. */
3832  function buildSectionList(opts) {
3833    if (opts.section && SECTION_BY_ID[opts.section]) {
3834      return SECTION_BY_ID[opts.section].sections || [];
3835    }
3836    return [];
3837  }
3838
3839  function buildResourceList(opts) {
3840    return $.queryResources(opts);
3841  }
3842
3843  $.queryResources = function(opts) {
3844    var maxResults = opts.maxResults || 100;
3845
3846    var query = opts.query || '';
3847    var expressions = parseResourceQuery(query);
3848    var addedResourceIndices = {};
3849    var results = [];
3850
3851    for (var i = 0; i < expressions.length; i++) {
3852      var clauses = expressions[i];
3853
3854      // build initial set of resources from first clause
3855      var firstClause = clauses[0];
3856      var resources = [];
3857      switch (firstClause.attr) {
3858        case 'type':
3859          resources = ALL_RESOURCES_BY_TYPE[firstClause.value];
3860          break;
3861        case 'lang':
3862          resources = ALL_RESOURCES_BY_LANG[firstClause.value];
3863          break;
3864        case 'tag':
3865          resources = ALL_RESOURCES_BY_TAG[firstClause.value];
3866          break;
3867        case 'collection':
3868          var urls = RESOURCE_COLLECTIONS[firstClause.value].resources || [];
3869          resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; });
3870          break;
3871        case 'section':
3872          var urls = SITE_MAP[firstClause.value].sections || [];
3873          resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; });
3874          break;
3875      }
3876      // console.log(firstClause.attr + ':' + firstClause.value);
3877      resources = resources || [];
3878
3879      // use additional clauses to filter corpus
3880      if (clauses.length > 1) {
3881        var otherClauses = clauses.slice(1);
3882        resources = resources.filter(getResourceMatchesClausesFilter(otherClauses));
3883      }
3884
3885      // filter out resources already added
3886      if (i > 1) {
3887        resources = resources.filter(getResourceNotAlreadyAddedFilter(addedResourceIndices));
3888      }
3889
3890      // add to list of already added indices
3891      for (var j = 0; j < resources.length; j++) {
3892        if (resources[j]) {
3893          addedResourceIndices[resources[j].index] = 1;
3894        }
3895      }
3896
3897      // concat to final results list
3898      results = results.concat(resources);
3899    }
3900
3901    if (opts.sortOrder && results.length) {
3902      var attr = opts.sortOrder;
3903
3904      if (opts.sortOrder == 'random') {
3905        var i = results.length, j, temp;
3906        while (--i) {
3907          j = Math.floor(Math.random() * (i + 1));
3908          temp = results[i];
3909          results[i] = results[j];
3910          results[j] = temp;
3911        }
3912      } else {
3913        var desc = attr.charAt(0) == '-';
3914        if (desc) {
3915          attr = attr.substring(1);
3916        }
3917        results = results.sort(function(x,y) {
3918          return (desc ? -1 : 1) * (parseInt(x[attr], 10) - parseInt(y[attr], 10));
3919        });
3920      }
3921    }
3922
3923    results = results.filter(getResourceNotAlreadyAddedFilter(addedPageResources));
3924    results = results.slice(0, maxResults);
3925
3926    for (var j = 0; j < results.length; ++j) {
3927      addedPageResources[results[j].index] = 1;
3928    }
3929
3930    return results;
3931  }
3932
3933
3934  function getResourceNotAlreadyAddedFilter(addedResourceIndices) {
3935    return function(resource) {
3936      return resource && !addedResourceIndices[resource.index];
3937    };
3938  }
3939
3940
3941  function getResourceMatchesClausesFilter(clauses) {
3942    return function(resource) {
3943      return doesResourceMatchClauses(resource, clauses);
3944    };
3945  }
3946
3947
3948  function doesResourceMatchClauses(resource, clauses) {
3949    for (var i = 0; i < clauses.length; i++) {
3950      var map;
3951      switch (clauses[i].attr) {
3952        case 'type':
3953          map = IS_RESOURCE_OF_TYPE[clauses[i].value];
3954          break;
3955        case 'lang':
3956          map = IS_RESOURCE_IN_LANG[clauses[i].value];
3957          break;
3958        case 'tag':
3959          map = IS_RESOURCE_TAGGED[clauses[i].value];
3960          break;
3961      }
3962
3963      if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) {
3964        return clauses[i].negative;
3965      }
3966    }
3967    return true;
3968  }
3969
3970  function cleanUrl(url)
3971  {
3972    if (url && url.indexOf('//') === -1) {
3973      url = toRoot + url;
3974    }
3975
3976    return url;
3977  }
3978
3979
3980  function parseResourceQuery(query) {
3981    // Parse query into array of expressions (expression e.g. 'tag:foo + type:video')
3982    var expressions = [];
3983    var expressionStrs = query.split(',') || [];
3984    for (var i = 0; i < expressionStrs.length; i++) {
3985      var expr = expressionStrs[i] || '';
3986
3987      // Break expression into clauses (clause e.g. 'tag:foo')
3988      var clauses = [];
3989      var clauseStrs = expr.split(/(?=[\+\-])/);
3990      for (var j = 0; j < clauseStrs.length; j++) {
3991        var clauseStr = clauseStrs[j] || '';
3992
3993        // Get attribute and value from clause (e.g. attribute='tag', value='foo')
3994        var parts = clauseStr.split(':');
3995        var clause = {};
3996
3997        clause.attr = parts[0].replace(/^\s+|\s+$/g,'');
3998        if (clause.attr) {
3999          if (clause.attr.charAt(0) == '+') {
4000            clause.attr = clause.attr.substring(1);
4001          } else if (clause.attr.charAt(0) == '-') {
4002            clause.negative = true;
4003            clause.attr = clause.attr.substring(1);
4004          }
4005        }
4006
4007        if (parts.length > 1) {
4008          clause.value = parts[1].replace(/^\s+|\s+$/g,'');
4009        }
4010
4011        clauses.push(clause);
4012      }
4013
4014      if (!clauses.length) {
4015        continue;
4016      }
4017
4018      expressions.push(clauses);
4019    }
4020
4021    return expressions;
4022  }
4023})();
4024
4025(function($) {
4026
4027  /*
4028    Utility method for creating dom for the description area of a card.
4029    Used in decorateResourceCard and decorateResource.
4030  */
4031  function buildResourceCardDescription(resource, plusone) {
4032    var $description = $('<div>').addClass('description ellipsis');
4033
4034    $description.append($('<div>').addClass('text').html(resource.summary));
4035
4036    if (resource.cta) {
4037      $description.append($('<a>').addClass('cta').html(resource.cta));
4038    }
4039
4040    if (plusone) {
4041      var plusurl = resource.url.indexOf("//") > -1 ? resource.url :
4042        "//developer.android.com/" + resource.url;
4043
4044      $description.append($('<div>').addClass('util')
4045        .append($('<div>').addClass('g-plusone')
4046          .attr('data-size', 'small')
4047          .attr('data-align', 'right')
4048          .attr('data-href', plusurl)));
4049    }
4050
4051    return $description;
4052  }
4053
4054
4055  /* Simple jquery function to create dom for a standard resource card */
4056  $.fn.decorateResourceCard = function(resource,plusone) {
4057    var section = resource.group || resource.type;
4058    var imgUrl = resource.image ||
4059      'assets/images/resource-card-default-android.jpg';
4060
4061    if (imgUrl.indexOf('//') === -1) {
4062      imgUrl = toRoot + imgUrl;
4063    }
4064
4065    if (resource.type === 'youtube') {
4066      $('<div>').addClass('play-button')
4067        .append($('<i class="dac-sprite dac-play-white">'))
4068        .appendTo(this);
4069    }
4070
4071    $('<div>').addClass('card-bg')
4072      .css('background-image', 'url(' + (imgUrl || toRoot +
4073        'assets/images/resource-card-default-android.jpg') + ')')
4074      .appendTo(this);
4075
4076    $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : ''))
4077      .append($('<div>').addClass('section').text(section))
4078      .append($('<div>').addClass('title').html(resource.title))
4079      .append(buildResourceCardDescription(resource, plusone))
4080      .appendTo(this);
4081
4082    return this;
4083  };
4084
4085  /* Simple jquery function to create dom for a resource section card (menu) */
4086  $.fn.decorateResourceSection = function(section,plusone) {
4087    var resource = section.resource;
4088    //keep url clean for matching and offline mode handling
4089    var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot;
4090    var $base = $('<a>')
4091        .addClass('card-bg')
4092        .attr('href', resource.url)
4093        .append($('<div>').addClass('card-section-icon')
4094          .append($('<div>').addClass('icon'))
4095          .append($('<div>').addClass('section').html(resource.title)))
4096      .appendTo(this);
4097
4098    var $cardInfo = $('<div>').addClass('card-info').appendTo(this);
4099
4100    if (section.sections && section.sections.length) {
4101      // Recurse the section sub-tree to find a resource image.
4102      var stack = [section];
4103
4104      while (stack.length) {
4105        if (stack[0].resource.image) {
4106          $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')');
4107          break;
4108        }
4109
4110        if (stack[0].sections) {
4111          stack = stack.concat(stack[0].sections);
4112        }
4113
4114        stack.shift();
4115      }
4116
4117      var $ul = $('<ul>')
4118        .appendTo($cardInfo);
4119
4120      var max = section.sections.length > 3 ? 3 : section.sections.length;
4121
4122      for (var i = 0; i < max; ++i) {
4123
4124        var subResource = section.sections[i];
4125        if (!plusone) {
4126          $('<li>')
4127            .append($('<a>').attr('href', subResource.url)
4128              .append($('<div>').addClass('title').html(subResource.title))
4129              .append($('<div>').addClass('description ellipsis')
4130                .append($('<div>').addClass('text').html(subResource.summary))
4131                .append($('<div>').addClass('util'))))
4132          .appendTo($ul);
4133        } else {
4134          $('<li>')
4135            .append($('<a>').attr('href', subResource.url)
4136              .append($('<div>').addClass('title').html(subResource.title))
4137              .append($('<div>').addClass('description ellipsis')
4138                .append($('<div>').addClass('text').html(subResource.summary))
4139                .append($('<div>').addClass('util')
4140                  .append($('<div>').addClass('g-plusone')
4141                    .attr('data-size', 'small')
4142                    .attr('data-align', 'right')
4143                    .attr('data-href', resource.url)))))
4144          .appendTo($ul);
4145        }
4146      }
4147
4148      // Add a more row
4149      if (max < section.sections.length) {
4150        $('<li>')
4151          .append($('<a>').attr('href', resource.url)
4152            .append($('<div>')
4153              .addClass('title')
4154              .text('More')))
4155        .appendTo($ul);
4156      }
4157    } else {
4158      // No sub-resources, just render description?
4159    }
4160
4161    return this;
4162  };
4163
4164
4165
4166
4167  /* Render other types of resource styles that are not cards. */
4168  $.fn.decorateResource = function(resource, opts) {
4169    var imgUrl = resource.image ||
4170      'assets/images/resource-card-default-android.jpg';
4171    var linkUrl = resource.url;
4172
4173    if (imgUrl.indexOf('//') === -1) {
4174      imgUrl = toRoot + imgUrl;
4175    }
4176
4177    if (linkUrl && linkUrl.indexOf('//') === -1) {
4178      linkUrl = toRoot + linkUrl;
4179    }
4180
4181    $(this).append(
4182      $('<div>').addClass('image')
4183        .css('background-image', 'url(' + imgUrl + ')'),
4184      $('<div>').addClass('info').append(
4185        $('<h4>').addClass('title').html(resource.title),
4186        $('<p>').addClass('summary').html(resource.summary),
4187        $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More')
4188      )
4189    );
4190
4191    return this;
4192  };
4193})(jQuery);
4194
4195
4196/* Calculate the vertical area remaining */
4197(function($) {
4198    $.fn.ellipsisfade= function(lineHeight) {
4199        this.each(function() {
4200            // get element text
4201            var $this = $(this);
4202            var remainingHeight = $this.parent().parent().height();
4203            $this.parent().siblings().each(function ()
4204            {
4205              if ($(this).is(":visible")) {
4206                var h = $(this).outerHeight(true);
4207                remainingHeight = remainingHeight - h;
4208              }
4209            });
4210
4211            adjustedRemainingHeight = ((remainingHeight)/lineHeight>>0)*lineHeight
4212            $this.parent().css({'height': adjustedRemainingHeight});
4213            $this.css({'height': "auto"});
4214        });
4215
4216        return this;
4217    };
4218}) (jQuery);
4219
4220/*
4221  Fullscreen Carousel
4222
4223  The following allows for an area at the top of the page that takes over the
4224  entire browser height except for its top offset and an optional bottom
4225  padding specified as a data attribute.
4226
4227  HTML:
4228
4229  <div class="fullscreen-carousel">
4230    <div class="fullscreen-carousel-content">
4231      <!-- content here -->
4232    </div>
4233    <div class="fullscreen-carousel-content">
4234      <!-- content here -->
4235    </div>
4236
4237    etc ...
4238
4239  </div>
4240
4241  Control over how the carousel takes over the screen can mostly be defined in
4242  a css file. Setting min-height on the .fullscreen-carousel-content elements
4243  will prevent them from shrinking to far vertically when the browser is very
4244  short, and setting max-height on the .fullscreen-carousel itself will prevent
4245  the area from becoming to long in the case that the browser is stretched very
4246  tall.
4247
4248  There is limited functionality for having multiple sections since that request
4249  was removed, but it is possible to add .next-arrow and .prev-arrow elements to
4250  scroll between multiple content areas.
4251*/
4252
4253(function() {
4254  $(document).ready(function() {
4255    $('.fullscreen-carousel').each(function() {
4256      initWidget(this);
4257    });
4258  });
4259
4260  function initWidget(widget) {
4261    var $widget = $(widget);
4262
4263    var topOffset = $widget.offset().top;
4264    var padBottom = parseInt($widget.data('paddingbottom')) || 0;
4265    var maxHeight = 0;
4266    var minHeight = 0;
4267    var $content = $widget.find('.fullscreen-carousel-content');
4268    var $nextArrow = $widget.find('.next-arrow');
4269    var $prevArrow = $widget.find('.prev-arrow');
4270    var $curSection = $($content[0]);
4271
4272    if ($content.length <= 1) {
4273      $nextArrow.hide();
4274      $prevArrow.hide();
4275    } else {
4276      $nextArrow.click(function() {
4277        var index = ($content.index($curSection) + 1);
4278        $curSection.hide();
4279        $curSection = $($content[index >= $content.length ? 0 : index]);
4280        $curSection.show();
4281      });
4282
4283      $prevArrow.click(function() {
4284        var index = ($content.index($curSection) - 1);
4285        $curSection.hide();
4286        $curSection = $($content[index < 0 ? $content.length - 1 : 0]);
4287        $curSection.show();
4288      });
4289    }
4290
4291    // Just hide all content sections except first.
4292    $content.each(function(index) {
4293      if ($(this).height() > minHeight) minHeight = $(this).height();
4294      $(this).css({position: 'absolute',  display: index > 0 ? 'none' : ''});
4295    });
4296
4297    // Register for changes to window size, and trigger.
4298    $(window).resize(resizeWidget);
4299    resizeWidget();
4300
4301    function resizeWidget() {
4302      var height = $(window).height() - topOffset - padBottom;
4303      $widget.width($(window).width());
4304      $widget.height(height < minHeight ? minHeight :
4305        (maxHeight && height > maxHeight ? maxHeight : height));
4306    }
4307  }
4308})();
4309
4310
4311
4312
4313
4314/*
4315  Tab Carousel
4316
4317  The following allows tab widgets to be installed via the html below. Each
4318  tab content section should have a data-tab attribute matching one of the
4319  nav items'. Also each tab content section should have a width matching the
4320  tab carousel.
4321
4322  HTML:
4323
4324  <div class="tab-carousel">
4325    <ul class="tab-nav">
4326      <li><a href="#" data-tab="handsets">Handsets</a>
4327      <li><a href="#" data-tab="wearable">Wearable</a>
4328      <li><a href="#" data-tab="tv">TV</a>
4329    </ul>
4330
4331    <div class="tab-carousel-content">
4332      <div data-tab="handsets">
4333        <!--Full width content here-->
4334      </div>
4335
4336      <div data-tab="wearable">
4337        <!--Full width content here-->
4338      </div>
4339
4340      <div data-tab="tv">
4341        <!--Full width content here-->
4342      </div>
4343    </div>
4344  </div>
4345
4346*/
4347(function() {
4348  $(document).ready(function() {
4349    $('.tab-carousel').each(function() {
4350      initWidget(this);
4351    });
4352  });
4353
4354  function initWidget(widget) {
4355    var $widget = $(widget);
4356    var $nav = $widget.find('.tab-nav');
4357    var $anchors = $nav.find('[data-tab]');
4358    var $li = $nav.find('li');
4359    var $contentContainer = $widget.find('.tab-carousel-content');
4360    var $tabs = $contentContainer.find('[data-tab]');
4361    var $curTab = $($tabs[0]); // Current tab is first tab.
4362    var width = $widget.width();
4363
4364    // Setup nav interactivity.
4365    $anchors.click(function(evt) {
4366      evt.preventDefault();
4367      var query = '[data-tab=' + $(this).data('tab') + ']';
4368      transitionWidget($tabs.filter(query));
4369    });
4370
4371    // Add highlight for navigation on first item.
4372    var $highlight = $('<div>').addClass('highlight')
4373      .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'})
4374      .appendTo($nav);
4375
4376    // Store height since we will change contents to absolute.
4377    $contentContainer.height($contentContainer.height());
4378
4379    // Absolutely position tabs so they're ready for transition.
4380    $tabs.each(function(index) {
4381      $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'});
4382    });
4383
4384    function transitionWidget($toTab) {
4385      if (!$curTab.is($toTab)) {
4386        var curIndex = $tabs.index($curTab[0]);
4387        var toIndex = $tabs.index($toTab[0]);
4388        var dir = toIndex > curIndex ? 1 : -1;
4389
4390        // Animate content sections.
4391        $toTab.css({left:(width * dir) + 'px'});
4392        $curTab.animate({left:(width * -dir) + 'px'});
4393        $toTab.animate({left:'0'});
4394
4395        // Animate navigation highlight.
4396        $highlight.animate({left:$($li[toIndex]).position().left + 'px',
4397          width:$($li[toIndex]).outerWidth() + 'px'})
4398
4399        // Store new current section.
4400        $curTab = $toTab;
4401      }
4402    }
4403  }
4404})();
4405
4406/**
4407 * Auto TOC
4408 *
4409 * Upgrades h2s on the page to have a rule and be toggle-able on mobile.
4410 */
4411(function($) {
4412  var upgraded = false;
4413  var h2Titles;
4414
4415  function initWidget() {
4416    // add HRs below all H2s (except for a few other h2 variants)
4417    // Consider doing this with css instead.
4418    h2Titles = $('h2').not('#qv h2, #tb h2, .sidebox h2, #devdoc-nav h2, h2.norule');
4419    h2Titles.css({marginBottom:0}).after('<hr/>');
4420
4421    // Exit early if on older browser.
4422    if (!window.matchMedia) {
4423      return;
4424    }
4425
4426    // Only run logic in mobile layout.
4427    var query = window.matchMedia('(max-width: 719px)');
4428    if (query.matches) {
4429      makeTogglable();
4430    } else {
4431      query.addListener(makeTogglable);
4432    }
4433  }
4434
4435  function makeTogglable() {
4436    // Only run this logic once.
4437    if (upgraded) { return; }
4438    upgraded = true;
4439
4440    // Only make content h2s togglable.
4441    var contentTitles = h2Titles.filter('#jd-content *');
4442
4443    // If there are more than 1
4444    if (contentTitles.size() < 2) {
4445      return;
4446    }
4447
4448    contentTitles.each(function() {
4449      // Find all the relevant nodes.
4450      var $title = $(this);
4451      var $hr = $title.next();
4452      var $contents = $hr.nextUntil('h2, .next-docs');
4453      var $section = $($title)
4454        .add($hr)
4455        .add($title.prev('a[name]'))
4456        .add($contents);
4457      var $anchor = $section.first().prev();
4458      var anchorMethod = 'after';
4459      if ($anchor.length === 0) {
4460        $anchor = $title.parent();
4461        anchorMethod = 'prepend';
4462      }
4463
4464      // Some h2s are in their own container making it pretty hard to find the end, so skip.
4465      if ($contents.length === 0) {
4466        return;
4467      }
4468
4469      // Remove from DOM before messing with it. DOM is slow!
4470      $section.detach();
4471
4472      // Add mobile-only expand arrows.
4473      $title.prepend('<span class="dac-visible-mobile-inline-block">' +
4474          '<i class="dac-toggle-expand dac-sprite dac-expand-more-black"></i>' +
4475          '<i class="dac-toggle-collapse dac-sprite dac-expand-less-black"></i>' +
4476          '</span>')
4477        .attr('data-toggle', 'section');
4478
4479      // Wrap in magic markup.
4480      $section = $section.wrapAll('<div class="dac-toggle dac-mobile">').parent();
4481      $contents.wrapAll('<div class="dac-toggle-content"><div>'); // extra div used for max-height calculation.
4482
4483      // Pre-expand section if requested.
4484      if ($title.hasClass('is-expanded')) {
4485        $section.addClass('is-expanded');
4486      }
4487
4488      // Pre-expand section if targetted by hash.
4489      if (location.hash && $section.find(location.hash).length) {
4490        $section.addClass('is-expanded');
4491      }
4492
4493      // Add it back to the dom.
4494      $anchor[anchorMethod].call($anchor, $section);
4495    });
4496  }
4497
4498  $(function() {
4499    initWidget();
4500  });
4501})(jQuery);
4502
4503(function($) {
4504  'use strict';
4505
4506  /**
4507   * Toggle Floating Label state.
4508   * @param {HTMLElement} el - The DOM element.
4509   * @param options
4510   * @constructor
4511   */
4512  function FloatingLabel(el, options) {
4513    this.el = $(el);
4514    this.options = $.extend({}, FloatingLabel.DEFAULTS_, options);
4515    this.group = this.el.closest('.dac-form-input-group');
4516    this.input = this.group.find('.dac-form-input');
4517
4518    this.checkValue_ = this.checkValue_.bind(this);
4519    this.checkValue_();
4520
4521    this.input.on('focus', function() {
4522      this.group.addClass('dac-focused');
4523    }.bind(this));
4524    this.input.on('blur', function() {
4525      this.group.removeClass('dac-focused');
4526      this.checkValue_();
4527    }.bind(this));
4528    this.input.on('keyup', this.checkValue_);
4529  }
4530
4531  /**
4532   * The label is moved out of the textbox when it has a value.
4533   */
4534  FloatingLabel.prototype.checkValue_ = function() {
4535    if (this.input.val().length) {
4536      this.group.addClass('dac-has-value');
4537    } else {
4538      this.group.removeClass('dac-has-value');
4539    }
4540  };
4541
4542  /**
4543   * jQuery plugin
4544   * @param  {object} options - Override default options.
4545   */
4546  $.fn.dacFloatingLabel = function(options) {
4547    return this.each(function() {
4548      new FloatingLabel(this, options);
4549    });
4550  };
4551
4552  $(document).on('ready.aranja', function() {
4553    $('.dac-form-floatlabel').each(function() {
4554      $(this).dacFloatingLabel($(this).data());
4555    });
4556  });
4557})(jQuery);
4558
4559/* global toRoot, CAROUSEL_OVERRIDE */
4560(function($) {
4561  // Ordering matters
4562  var TAG_MAP = [
4563    {from: 'developerstory', to: 'Android Developer Story'},
4564    {from: 'googleplay', to: 'Google Play'}
4565  ];
4566
4567  function DacCarouselQuery(el) {
4568    this.el = $(el);
4569
4570    var opts = this.el.data();
4571    opts.maxResults = parseInt(opts.maxResults || '100', 10);
4572    opts.query = opts.carouselQuery;
4573    var resources = $.queryResources(opts);
4574
4575    this.el.empty();
4576    $(resources).map(function() {
4577      var resource = $.extend({}, this, CAROUSEL_OVERRIDE[this.url]);
4578      var slide = $('<article class="dac-expand dac-hero">');
4579      var image = cleanUrl(resource.heroImage || resource.image);
4580      var fullBleed = image && !resource.heroColor;
4581
4582      // Configure background
4583      slide.css({
4584        backgroundImage: fullBleed ? 'url(' + image + ')' : '',
4585        backgroundColor: resource.heroColor || ''
4586      });
4587
4588      // Should copy be inverted
4589      slide.toggleClass('dac-invert', resource.heroInvert || fullBleed);
4590      slide.toggleClass('dac-darken', fullBleed);
4591
4592      // Should be clickable
4593      slide.append($('<a class="dac-hero-carousel-action">').attr('href', cleanUrl(resource.url)));
4594
4595      var cols = $('<div class="cols dac-hero-content">');
4596
4597      // inline image column
4598      var rightCol = $('<div class="col-1of2 col-push-1of2 dac-hero-figure">')
4599        .appendTo(cols);
4600
4601      if (!fullBleed && image) {
4602        rightCol.append($('<img>').attr('src', image));
4603      }
4604
4605      // info column
4606      $('<div class="col-1of2 col-pull-1of2">')
4607        .append($('<div class="dac-hero-tag">').text(formatTag(resource)))
4608        .append($('<h1 class="dac-hero-title">').text(formatTitle(resource)))
4609        .append($('<p class="dac-hero-description">').text(resource.summary))
4610        .append($('<a class="dac-hero-cta">')
4611          .text(formatCTA(resource))
4612          .attr('href', cleanUrl(resource.url))
4613          .prepend($('<span class="dac-sprite dac-auto-chevron">'))
4614        )
4615        .appendTo(cols);
4616
4617      slide.append(cols.wrap('<div class="wrap">').parent());
4618      return slide[0];
4619    }).prependTo(this.el);
4620
4621    // Pagination element.
4622    this.el.append('<div class="dac-hero-carousel-pagination"><div class="wrap" data-carousel-pagination>');
4623
4624    this.el.dacCarousel();
4625  }
4626
4627  function cleanUrl(url) {
4628    if (url && url.indexOf('//') === -1) {
4629      url = toRoot + url;
4630    }
4631    return url;
4632  }
4633
4634  function formatTag(resource) {
4635    // Hmm, need a better more scalable solution for this.
4636    for (var i = 0, mapping; mapping = TAG_MAP[i]; i++) {
4637      if (resource.tags.indexOf(mapping.from) > -1) {
4638        return mapping.to;
4639      }
4640    }
4641    return resource.type;
4642  }
4643
4644  function formatTitle(resource) {
4645    return resource.title.replace(/android developer story: /i, '');
4646  }
4647
4648  function formatCTA(resource) {
4649    return resource.type === 'youtube' ? 'Watch the video' : 'Learn more';
4650  }
4651
4652  // jQuery plugin
4653  $.fn.dacCarouselQuery = function() {
4654    return this.each(function() {
4655      var el = $(this);
4656      var data = el.data('dac.carouselQuery');
4657
4658      if (!data) { el.data('dac.carouselQuery', (data = new DacCarouselQuery(el))); }
4659    });
4660  };
4661
4662  // Data API
4663  $(function() {
4664    $('[data-carousel-query]').dacCarouselQuery();
4665  });
4666})(jQuery);
4667
4668(function($) {
4669  /**
4670   * A CSS based carousel, inspired by SequenceJS.
4671   * @param {jQuery} el
4672   * @param {object} options
4673   * @constructor
4674   */
4675  function DacCarousel(el, options) {
4676    this.el = $(el);
4677    this.options = options = $.extend({}, DacCarousel.OPTIONS, this.el.data(), options || {});
4678    this.frames = this.el.find(options.frameSelector);
4679    this.count = this.frames.size();
4680    this.current = options.start;
4681
4682    this.initPagination();
4683    this.initEvents();
4684    this.initFrame();
4685  }
4686
4687  DacCarousel.OPTIONS = {
4688    auto:      true,
4689    autoTime:  10000,
4690    autoMinTime: 5000,
4691    btnPrev:   '[data-carousel-prev]',
4692    btnNext:   '[data-carousel-next]',
4693    frameSelector: 'article',
4694    loop:      true,
4695    start:     0,
4696    swipeThreshold: 160,
4697    pagination: '[data-carousel-pagination]'
4698  };
4699
4700  DacCarousel.prototype.initPagination = function() {
4701    this.pagination = $([]);
4702    if (!this.options.pagination) { return; }
4703
4704    var pagination = $('<ul class="dac-pagination">');
4705    var parent = this.el;
4706    if (typeof this.options.pagination === 'string') { parent = this.el.find(this.options.pagination); }
4707
4708    if (this.count > 1) {
4709      for (var i = 0; i < this.count; i++) {
4710        var li = $('<li class="dac-pagination-item">').text(i);
4711        if (i === this.options.start) { li.addClass('active'); }
4712        li.click(this.go.bind(this, i));
4713
4714        pagination.append(li);
4715      }
4716      this.pagination = pagination.children();
4717      parent.append(pagination);
4718    }
4719  };
4720
4721  DacCarousel.prototype.initEvents = function() {
4722    var that = this;
4723
4724    this.touch = {
4725      start: {x: 0, y: 0},
4726      end:   {x: 0, y: 0}
4727    };
4728
4729    this.el.on('touchstart', this.touchstart_.bind(this));
4730    this.el.on('touchend', this.touchend_.bind(this));
4731    this.el.on('touchmove', this.touchmove_.bind(this));
4732
4733    this.el.hover(function() {
4734      that.pauseRotateTimer();
4735    }, function() {
4736      that.startRotateTimer();
4737    });
4738
4739    $(this.options.btnPrev).click(function(e) {
4740      e.preventDefault();
4741      that.prev();
4742    });
4743
4744    $(this.options.btnNext).click(function(e) {
4745      e.preventDefault();
4746      that.next();
4747    });
4748  };
4749
4750  DacCarousel.prototype.touchstart_ = function(event) {
4751    var t = event.originalEvent.touches[0];
4752    this.touch.start = {x: t.screenX, y: t.screenY};
4753  };
4754
4755  DacCarousel.prototype.touchend_ = function() {
4756    var deltaX = this.touch.end.x - this.touch.start.x;
4757    var deltaY = Math.abs(this.touch.end.y - this.touch.start.y);
4758    var shouldSwipe = (deltaY < Math.abs(deltaX)) && (Math.abs(deltaX) >= this.options.swipeThreshold);
4759
4760    if (shouldSwipe) {
4761      if (deltaX > 0) {
4762        this.prev();
4763      } else {
4764        this.next();
4765      }
4766    }
4767  };
4768
4769  DacCarousel.prototype.touchmove_ = function(event) {
4770    var t = event.originalEvent.touches[0];
4771    this.touch.end = {x: t.screenX, y: t.screenY};
4772  };
4773
4774  DacCarousel.prototype.initFrame = function() {
4775    this.frames.removeClass('active').eq(this.options.start).addClass('active');
4776  };
4777
4778  DacCarousel.prototype.startRotateTimer = function() {
4779    if (!this.options.auto || this.rotateTimer) { return; }
4780    this.rotateTimer = setTimeout(this.next.bind(this), this.options.autoTime);
4781  };
4782
4783  DacCarousel.prototype.pauseRotateTimer = function() {
4784    clearTimeout(this.rotateTimer);
4785    this.rotateTimer = null;
4786  };
4787
4788  DacCarousel.prototype.prev = function() {
4789    this.go(this.current - 1);
4790  };
4791
4792  DacCarousel.prototype.next = function() {
4793    this.go(this.current + 1);
4794  };
4795
4796  DacCarousel.prototype.go = function(next) {
4797    // Figure out what the next slide is.
4798    while (this.count > 0 && next >= this.count) { next -= this.count; }
4799    while (next < 0) { next += this.count; }
4800
4801    // Cancel if we're already on that slide.
4802    if (next === this.current) { return; }
4803
4804    // Prepare next slide.
4805    this.frames.eq(next).removeClass('out');
4806
4807    // Recalculate styles before starting slide transition.
4808    this.el.resolveStyles();
4809    // Update pagination
4810    this.pagination.removeClass('active').eq(next).addClass('active');
4811
4812    // Transition out current frame
4813    this.frames.eq(this.current).toggleClass('active out');
4814
4815    // Transition in a new frame
4816    this.frames.eq(next).toggleClass('active');
4817
4818    this.current = next;
4819  };
4820
4821  // Helper which resolves new styles for an element, so it can start transitioning
4822  // from the new values.
4823  $.fn.resolveStyles = function() {
4824    /*jshint expr:true*/
4825    this[0] && this[0].offsetTop;
4826    return this;
4827  };
4828
4829  // jQuery plugin
4830  $.fn.dacCarousel = function() {
4831    this.each(function() {
4832      var $el = $(this);
4833      $el.data('dac-carousel', new DacCarousel(this));
4834    });
4835    return this;
4836  };
4837
4838  // Data API
4839  $(function() {
4840    $('[data-carousel]').dacCarousel();
4841  });
4842})(jQuery);
4843
4844(function($) {
4845  'use strict';
4846
4847  function Modal(el, options) {
4848    this.el = $(el);
4849    this.options = $.extend({}, ToggleModal.DEFAULTS_, options);
4850    this.isOpen = false;
4851
4852    this.el.on('click', function(event) {
4853      if (!$.contains($('.dac-modal-window')[0], event.target)) {
4854        return this.el.trigger('modal-close');
4855      }
4856    }.bind(this));
4857
4858    this.el.on('modal-open', this.open_.bind(this));
4859    this.el.on('modal-close', this.close_.bind(this));
4860    this.el.on('modal-toggle', this.toggle_.bind(this));
4861  }
4862
4863  Modal.prototype.toggle_ = function() {
4864    this.el.trigger('modal-' + (this.isOpen ? 'close' : 'open'));
4865  };
4866
4867  Modal.prototype.close_ = function() {
4868    this.el.removeClass('dac-active');
4869    $('body').removeClass('dac-modal-open');
4870    this.isOpen = false;
4871  };
4872
4873  Modal.prototype.open_ = function() {
4874    this.el.addClass('dac-active');
4875    $('body').addClass('dac-modal-open');
4876    this.isOpen = true;
4877  };
4878
4879  function ToggleModal(el, options) {
4880    this.el = $(el);
4881    this.options = $.extend({}, ToggleModal.DEFAULTS_, options);
4882    this.modal = this.options.modalToggle ? $('[data-modal="' + this.options.modalToggle + '"]') :
4883      this.el.closest('[data-modal]');
4884
4885    this.el.on('click', this.clickHandler_.bind(this));
4886  }
4887
4888  ToggleModal.prototype.clickHandler_ = function(event) {
4889    event.preventDefault();
4890    this.modal.trigger('modal-toggle');
4891  };
4892
4893  /**
4894   * jQuery plugin
4895   * @param  {object} options - Override default options.
4896   */
4897  $.fn.dacModal = function(options) {
4898    return this.each(function() {
4899      new Modal(this, options);
4900    });
4901  };
4902
4903  $.fn.dacToggleModal = function(options) {
4904    return this.each(function() {
4905      new ToggleModal(this, options);
4906    });
4907  };
4908
4909  /**
4910   * Data Attribute API
4911   */
4912  $(document).on('ready.aranja', function() {
4913    $('[data-modal]').each(function() {
4914      $(this).dacModal($(this).data());
4915    });
4916
4917    $('[data-modal-toggle]').each(function() {
4918      $(this).dacToggleModal($(this).data());
4919    });
4920  });
4921})(jQuery);
4922
4923(function($) {
4924  'use strict';
4925
4926  /**
4927   * Toggle the visabilty of the mobile navigation.
4928   * @param {HTMLElement} el - The DOM element.
4929   * @param options
4930   * @constructor
4931   */
4932  function ToggleNav(el, options) {
4933    this.el = $(el);
4934    this.options = $.extend({}, ToggleNav.DEFAULTS_, options);
4935    this.options.target = [this.options.navigation];
4936
4937    if (this.options.body) {this.options.target.push('body')}
4938    if (this.options.dimmer) {this.options.target.push(this.options.dimmer)}
4939
4940    this.el.on('click', this.clickHandler_.bind(this));
4941  }
4942
4943  /**
4944   * ToggleNav Default Settings
4945   * @type {{body: boolean, dimmer: string, navigation: string, toggleClass: string}}
4946   * @private
4947   */
4948  ToggleNav.DEFAULTS_ = {
4949    body: true,
4950    dimmer: '.dac-nav-dimmer',
4951    navigation: '[data-dac-nav]',
4952    toggleClass: 'dac-nav-open'
4953  };
4954
4955  /**
4956   * The actual toggle logic.
4957   * @param event
4958   * @private
4959   */
4960  ToggleNav.prototype.clickHandler_ = function(event) {
4961    event.preventDefault();
4962    $(this.options.target.join(', ')).toggleClass(this.options.toggleClass);
4963  };
4964
4965  /**
4966   * jQuery plugin
4967   * @param  {object} options - Override default options.
4968   */
4969  $.fn.dacToggleMobileNav = function(options) {
4970    return this.each(function() {
4971      new ToggleNav(this, options);
4972    });
4973  };
4974
4975  /**
4976   * Data Attribute API
4977   */
4978  $(window).on('load.aranja', function() {
4979    $('[data-dac-toggle-nav]').each(function() {
4980      $(this).dacToggleMobileNav($(this).data());
4981    });
4982  });
4983})(jQuery);
4984
4985(function($) {
4986  'use strict';
4987
4988  /**
4989   * Submit the newsletter form to a Google Form.
4990   * @param {HTMLElement} el - The Form DOM element.
4991   * @constructor
4992   */
4993  function NewsletterForm(el) {
4994    this.el = $(el);
4995    this.form = this.el.find('form');
4996    $('<iframe/>').hide()
4997      .attr('name', 'dac-newsletter-iframe')
4998      .attr('src', '')
4999      .insertBefore(this.form);
5000    this.form.on('submit', this.submitHandler_.bind(this));
5001  }
5002
5003  /**
5004   * Milliseconds until modal has vanished after modal-close is triggered.
5005   * @type {number}
5006   * @private
5007   */
5008  NewsletterForm.CLOSE_DELAY_ = 300;
5009
5010  /**
5011   * Switch view to display form after close.
5012   * @private
5013   */
5014  NewsletterForm.prototype.closeHandler_ = function() {
5015    setTimeout(function() {
5016      this.el.trigger('swap-reset');
5017    }.bind(this), NewsletterForm.CLOSE_DELAY_);
5018  };
5019
5020  /**
5021   * Reset the modal to initial state.
5022   * @private
5023   */
5024  NewsletterForm.prototype.reset_ = function() {
5025    this.form.trigger('reset');
5026    this.el.one('modal-close', this.closeHandler_.bind(this));
5027  };
5028
5029  /**
5030   * Display a success view on submit.
5031   * @private
5032   */
5033  NewsletterForm.prototype.submitHandler_ = function() {
5034    this.el.one('swap-complete', this.reset_.bind(this));
5035    this.el.trigger('swap-content');
5036  };
5037
5038  /**
5039   * jQuery plugin
5040   * @param  {object} options - Override default options.
5041   */
5042  $.fn.dacNewsletterForm = function(options) {
5043    return this.each(function() {
5044      new NewsletterForm(this, options);
5045    });
5046  };
5047
5048  /**
5049   * Data Attribute API
5050   */
5051  $(document).on('ready.aranja', function() {
5052    $('[data-newsletter]').each(function() {
5053      $(this).dacNewsletterForm();
5054    });
5055  });
5056})(jQuery);
5057
5058(function($) {
5059  'use strict';
5060
5061  /**
5062   * Smoothly scroll to location on current page.
5063   * @param el
5064   * @param options
5065   * @constructor
5066   */
5067  function ScrollButton(el, options) {
5068    this.el = $(el);
5069    this.target = $(this.el.attr('href'));
5070    this.options = $.extend({}, ScrollButton.DEFAULTS_, options);
5071
5072    if (typeof this.options.offset === 'string') {
5073      this.options.offset = $(this.options.offset).height();
5074    }
5075
5076    this.el.on('click', this.clickHandler_.bind(this));
5077  }
5078
5079  /**
5080   * Default options
5081   * @type {{duration: number, easing: string, offset: number, scrollContainer: string}}
5082   * @private
5083   */
5084  ScrollButton.DEFAULTS_ = {
5085    duration: 300,
5086    easing: 'swing',
5087    offset: 0,
5088    scrollContainer: 'html, body'
5089  };
5090
5091  /**
5092   * Scroll logic
5093   * @param event
5094   * @private
5095   */
5096  ScrollButton.prototype.clickHandler_ = function(event) {
5097    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
5098      return;
5099    }
5100
5101    event.preventDefault();
5102
5103    $(this.options.scrollContainer).animate({
5104      scrollTop: this.target.offset().top - this.options.offset
5105    }, this.options);
5106  };
5107
5108  /**
5109   * jQuery plugin
5110   * @param  {object} options - Override default options.
5111   */
5112  $.fn.dacScrollButton = function(options) {
5113    return this.each(function() {
5114      new ScrollButton(this, options);
5115    });
5116  };
5117
5118  /**
5119   * Data Attribute API
5120   */
5121  $(document).on('ready.aranja', function() {
5122    $('[data-scroll-button]').each(function() {
5123      $(this).dacScrollButton($(this).data());
5124    });
5125  });
5126})(jQuery);
5127
5128(function($) {
5129  'use strict';
5130
5131  /**
5132   * A component that swaps two dynamic height views with an animation.
5133   * Listens for the following events:
5134   * * swap-content: triggers SwapContent.swap_()
5135   * * swap-reset: triggers SwapContent.reset()
5136   * @param el
5137   * @param options
5138   * @constructor
5139   */
5140  function SwapContent(el, options) {
5141    this.el = $(el);
5142    this.options = $.extend({}, SwapContent.DEFAULTS_, options);
5143    this.containers = this.el.find(this.options.container);
5144    this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0);
5145    this.el.on('swap-content', this.swap.bind(this));
5146    this.el.on('swap-reset', this.reset.bind(this));
5147  }
5148
5149  /**
5150   * SwapContent's default settings.
5151   * @type {{activeClass: string, container: string, transitionSpeed: number}}
5152   * @private
5153   */
5154  SwapContent.DEFAULTS_ = {
5155    activeClass: 'dac-active',
5156    container: '[data-swap-container]',
5157    transitionSpeed: 500
5158  };
5159
5160  /**
5161   * Returns container's visible height.
5162   * @param container
5163   * @returns {number}
5164   */
5165  SwapContent.prototype.currentHeight = function(container) {
5166    return container.children('.' + this.options.activeClass).outerHeight();
5167  };
5168
5169  /**
5170   * Reset to show initial content
5171   */
5172  SwapContent.prototype.reset = function() {
5173    if (!this.initiallyActive.hasClass(this.initiallyActive)) {
5174      this.containers.children().toggleClass(this.options.activeClass);
5175    }
5176  };
5177
5178  /**
5179   * Complete the swap.
5180   */
5181  SwapContent.prototype.complete = function() {
5182    this.containers.height('auto');
5183    this.containers.trigger('swap-complete');
5184  };
5185
5186  /**
5187   * Perform the swap of content.
5188   */
5189  SwapContent.prototype.swap = function() {
5190    console.log(this.containers);
5191    this.containers.each(function(index, container) {
5192      container = $(container);
5193      container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass);
5194      container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed,
5195        this.complete.bind(this));
5196    }.bind(this));
5197  };
5198
5199  /**
5200   * jQuery plugin
5201   * @param  {object} options - Override default options.
5202   */
5203  $.fn.dacSwapContent = function(options) {
5204    return this.each(function() {
5205      new SwapContent(this, options);
5206    });
5207  };
5208
5209  /**
5210   * Data Attribute API
5211   */
5212  $(document).on('ready.aranja', function() {
5213    $('[data-swap]').each(function() {
5214      $(this).dacSwapContent($(this).data());
5215    });
5216  });
5217})(jQuery);
5218
5219(function($) {
5220  function Toggle(el) {
5221    $(el).on('click.dac.togglesection', this.toggle);
5222  }
5223
5224  Toggle.prototype.toggle = function() {
5225    var $this = $(this);
5226
5227    var $parent = getParent($this);
5228    var isExpanded = $parent.hasClass('is-expanded');
5229
5230    transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded);
5231    $parent.toggleClass('is-expanded');
5232
5233    return false;
5234  };
5235
5236  function getParent($this) {
5237    var selector = $this.attr('data-target');
5238
5239    if (!selector) {
5240      selector = $this.attr('href');
5241      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
5242    }
5243
5244    var $parent = selector && $(selector);
5245
5246    $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle');
5247
5248    return $parent.length ? $parent : $this.parent();
5249  }
5250
5251  /**
5252   * Runs a transition of max-height along with responsive styles which hide or expand the element.
5253   * @param $el
5254   * @param visible
5255   */
5256  function transitionMaxHeight($el, visible) {
5257    var contentHeight = $el.prop('scrollHeight');
5258    var targetHeight = visible ? contentHeight : 0;
5259    var duration = $el.transitionDuration();
5260
5261    // If we're hiding, first set the maxHeight we're transitioning from.
5262    if (!visible) {
5263      $el.css('maxHeight', contentHeight + 'px')
5264        .resolveStyles();
5265    }
5266
5267    // Transition to new state
5268    $el.css('maxHeight', targetHeight);
5269
5270    // Reset maxHeight to css value after transition.
5271    setTimeout(function() {
5272      $el.css('maxHeight', '');
5273    }, duration);
5274  }
5275
5276  // Utility to get the transition duration for the element.
5277  $.fn.transitionDuration = function() {
5278    var d = $(this).css('transitionDuration') || '0s';
5279
5280    return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0);
5281  };
5282
5283  // jQuery plugin
5284  $.fn.toggleSection = function(option) {
5285    return this.each(function() {
5286      var $this = $(this);
5287      var data = $this.data('dac.togglesection');
5288      if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));}
5289      if (typeof option === 'string') {data[option].call($this);}
5290    });
5291  };
5292
5293  // Data api
5294  $(document)
5295    .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle);
5296})(jQuery);
5297