docs.js revision 843649bab10d2e6b8c9791132de7b6217dabed59
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 + 60;
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],p[" + 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/styles/disclosure_up.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/styles/disclosure_down.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  searchControl.query = query;
2552  searchControl.init();
2553  searchControl.trackSearchRequest(query);
2554  $("#searchResults").slideDown('slow', setStickyTop);
2555  return false;
2556}
2557
2558function hideResults() {
2559  $("#searchResults").slideUp('fast', setStickyTop);
2560  $("#search-close").addClass("hide");
2561  location.hash = '';
2562
2563  $("#search_autocomplete").val("").blur();
2564
2565  // reset the ajax search callback to nothing, so results don't appear unless ENTER
2566  searchControl.reset();
2567
2568  return false;
2569}
2570
2571/* ########################################################## */
2572/* ################  CUSTOM SEARCH ENGINE  ################## */
2573/* ########################################################## */
2574var searchControl = null;
2575var dacsearch = dacsearch || {};
2576
2577/**
2578 * The custom search engine API.
2579 * @constructor
2580 */
2581dacsearch.CustomSearchEngine = function() {
2582  /**
2583   * The last response from Google CSE.
2584   * @private {Object}
2585   */
2586  this.resultQuery_ = {};
2587
2588  /** @private {?Element} */
2589  this.searchResultEl_ = null;
2590
2591  /** @private {?Element} */
2592  this.searchInputEl_ = null;
2593
2594  /** @private {string} */
2595  this.query = '';
2596};
2597
2598/**
2599 * Initializes DAC's Google custom search engine.
2600 * @export
2601 */
2602dacsearch.CustomSearchEngine.prototype.init = function() {
2603  this.searchResultEl_ = $('#leftSearchControl');
2604  this.searchResultEl_.empty();
2605  this.searchInputEl_ = $('#search_autocomplete');
2606  this.searchInputEl_.focus().val(this.query);
2607  this.getResults_();
2608  this.bindEvents_();
2609};
2610
2611
2612/**
2613 * Binds the keyup event to the search input.
2614 * @private
2615 */
2616dacsearch.CustomSearchEngine.prototype.bindEvents_ = function() {
2617  this.searchInputEl_.keyup(this.debounce_(function(e) {
2618    var code = e.which;
2619    if (code != 13) {
2620      this.query = this.searchInputEl_.val();
2621      location.hash = 'q=' + encodeURI(this.query);
2622      this.searchResultEl_.empty();
2623      this.getResults_();
2624    }
2625  }.bind(this), 250));
2626};
2627
2628
2629/**
2630 * Resets the search control.
2631 */
2632dacsearch.CustomSearchEngine.prototype.reset = function() {
2633  this.query = '';
2634  this.searchInputEl_.off('keyup');
2635  this.searchResultEl_.empty();
2636  this.updateResultTitle_();
2637};
2638
2639
2640/**
2641 * Updates the search query text at the top of the results.
2642 * @private
2643 */
2644dacsearch.CustomSearchEngine.prototype.updateResultTitle_ = function() {
2645  $("#searchTitle").html("Results for <em>" + this.query + "</em>");
2646};
2647
2648
2649/**
2650 * Makes the CSE api call and gets the results.
2651 * @param {number=} opt_start The optional start index.
2652 * @private
2653 */
2654dacsearch.CustomSearchEngine.prototype.getResults_ = function(opt_start) {
2655  var lang = getLangPref();
2656  // Fix zh-cn to be zh-CN.
2657  lang = lang.replace(/-\w+/, function(m) { return m.toUpperCase(); });
2658  var cseUrl = 'https://content.googleapis.com/customsearch/v1?';
2659  var searchParams = {
2660    cx: '000521750095050289010:zpcpi1ea4s8',
2661    key: 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8',
2662    q: this.query,
2663    start: opt_start || 1,
2664    num: 6,
2665    hl: lang,
2666    fields: 'queries,items(pagemap,link,title,htmlSnippet,formattedUrl)'
2667  };
2668
2669  $.get(cseUrl + $.param(searchParams), function(data) {
2670    this.resultQuery_ = data;
2671    this.renderResults_(data);
2672    this.updateResultTitle_(this.query);
2673  }.bind(this));
2674};
2675
2676
2677/**
2678 * Renders the results.
2679 * @private
2680 */
2681dacsearch.CustomSearchEngine.prototype.renderResults_ = function(results) {
2682  var el = this.searchResultEl_;
2683
2684  if (!results.items) {
2685    el.append($('<div>').text('No results'));
2686    return;
2687  }
2688
2689  for (var i = 0; i < results.items.length; i++) {
2690    var item = results.items[i];
2691    var hasImage = item.pagemap && item.pagemap.cse_thumbnail;
2692    var sectionMatch = item.link.match(/developer\.android\.com\/(\w*)/);
2693    var section = (sectionMatch && sectionMatch[1]) || 'blog';
2694
2695    var entry = $('<div>').addClass('dac-custom-search-entry cols');
2696
2697    if (hasImage) {
2698      var image = item.pagemap.cse_thumbnail[0];
2699      entry.append($('<div>').addClass('col-1of6')
2700        .append($('<div>').addClass('dac-custom-search-image').css(
2701        'background-image', 'url(' + image.src + ')')));
2702    }
2703
2704    var linkTitleEl = $('<a>').text(item.title).attr('href', item.link);
2705    linkTitleEl.click(function(e) {
2706      ga('send', 'event', 'Google Custom Search',
2707          'clicked: ' + linkTitleEl.attr('href'),
2708          'query: ' + $("#search_autocomplete").val().toLowerCase());
2709    });
2710
2711    var linkUrlEl = $('<a>').addClass('dac-custom-search-link').text(
2712        item.formattedUrl).attr('href', item.link);
2713    linkUrlEl.click(function(e) {
2714      ga('send', 'event', 'Google Custom Search',
2715          'clicked: ' + linkUrlEl.attr('href'),
2716          'query: ' + $("#search_autocomplete").val().toLowerCase());
2717    });
2718
2719
2720    entry.append($('<div>').addClass(hasImage ? 'col-5of6' : 'col-6of6')
2721      .append($('<p>').addClass('dac-custom-search-section').text(section))
2722      .append(
2723        linkTitleEl.wrap('<h2>').parent().addClass('dac-custom-search-title'))
2724      .append($('<p>').addClass('dac-custom-search-snippet')
2725      .html(item.htmlSnippet.replace(/<br>/g, ''))).append(linkUrlEl));
2726
2727    el.append(entry);
2728  }
2729
2730  if ($('#dac-custom-search-load-more')) {
2731    $('#dac-custom-search-load-more').remove();
2732  }
2733
2734  if (results.queries.nextPage) {
2735    var loadMoreButton = $('<button id="dac-custom-search-load-more">')
2736      .addClass('dac-custom-search-load-more')
2737      .text('Load more')
2738      .click(function() {
2739        this.loadMoreResults_();
2740      }.bind(this));
2741
2742    el.append(loadMoreButton);
2743  }
2744};
2745
2746
2747/**
2748 * Loads more results.
2749 * @private
2750 */
2751dacsearch.CustomSearchEngine.prototype.loadMoreResults_ = function() {
2752  this.query = this.resultQuery_.queries.request[0].searchTerms;
2753  var start = this.resultQuery_.queries.nextPage[0].startIndex;
2754  var loadMoreButton = this.searchResultEl_.find(
2755      '#dac-custom-search-load-more');
2756  loadMoreButton.text('Loading more...');
2757  this.getResults_(start);
2758  this.trackSearchRequest(this.query + ' startIndex = ' + start);
2759};
2760
2761
2762/**
2763 * Tracks a search request.
2764 * @param {string} query The query for the request,
2765 *                       includes start index if loading more results.
2766 */
2767dacsearch.CustomSearchEngine.prototype.trackSearchRequest = function(query) {
2768  ga('send', 'event', 'Google Custom Search Submit', 'submit search query',
2769      'query: ' + query);
2770};
2771
2772
2773/**
2774 * Returns a function, that, as long as it continues to be invoked, will not
2775 * be triggered. The function will be called after it stops being called for
2776 * N milliseconds.
2777 * @param {Function} func The function to debounce.
2778 * @param {number} wait The number of milliseconds to wait before calling the function.
2779 * @private
2780 */
2781dacsearch.CustomSearchEngine.prototype.debounce_ = function(func, wait) {
2782  var timeout;
2783  return function() {
2784    var context = this, args = arguments;
2785    var later = function() {
2786      timeout = null;
2787      func.apply(context, args);
2788    };
2789   clearTimeout(timeout);
2790   timeout = setTimeout(later, wait);
2791  };
2792};
2793
2794
2795google.setOnLoadCallback(function(){
2796  searchControl = new dacsearch.CustomSearchEngine();
2797  if (location.hash.indexOf("q=") == -1) {
2798    // if there's no query in the url, don't search and make sure results are hidden
2799    $('#searchResults').hide();
2800    return;
2801  } else {
2802    // first time loading search results for this page
2803    searchControl.query = decodeURI(location.hash.split('q=')[1]);
2804    searchControl.init();
2805    searchControl.trackSearchRequest(searchControl.query);
2806    $('#searchResults').slideDown('slow', setStickyTop);
2807    $("#search-close").removeClass("hide");
2808  }
2809}, true);
2810
2811/* Adjust the scroll position to account for sticky header, only if the hash matches an id.
2812   This does not handle <a name=""> tags. Some CSS fixes those, but only for reference docs. */
2813function offsetScrollForSticky() {
2814  // Ignore if there's no search bar (some special pages have no header)
2815  if ($("#search-container").length < 1) return;
2816
2817  var hash = escape(location.hash.substr(1));
2818  var $matchingElement = $("#"+hash);
2819  // Sanity check that there's an element with that ID on the page
2820  if ($matchingElement.length) {
2821    // If the position of the target element is near the top of the page (<20px, where we expect it
2822    // to be because we need to move it down 60px to become in view), then move it down 60px
2823    if (Math.abs($matchingElement.offset().top - $(window).scrollTop()) < 20) {
2824      $(window).scrollTop($(window).scrollTop() - 60);
2825    }
2826  }
2827}
2828
2829// when an event on the browser history occurs (back, forward, load) requery hash and do search
2830$(window).hashchange( function(){
2831  // Ignore if there's no search bar (some special pages have no header)
2832  if ($("#search-container").length < 1) return;
2833
2834  // If the hash isn't a search query or there's an error in the query,
2835  // then adjust the scroll position to account for sticky header, then exit.
2836  if ((location.hash.indexOf("q=") == -1) || (searchControl.query == "undefined")) {
2837    // If the results pane is open, close it.
2838    if (!$("#searchResults").is(":hidden")) {
2839      hideResults();
2840    }
2841    offsetScrollForSticky();
2842    return;
2843  }
2844
2845  $('#searchResults').slideDown('slow', setStickyTop);
2846  $("#search_autocomplete").focus();
2847  $("#search-close").removeClass("hide");
2848});
2849
2850/* returns the given string with all HTML brackets converted to entities
2851    TODO: move this to the site's JS library */
2852function escapeHTML(string) {
2853  return string.replace(/</g,"&lt;")
2854                .replace(/>/g,"&gt;");
2855}
2856
2857
2858
2859
2860
2861
2862
2863/* ######################################################## */
2864/* #################  JAVADOC REFERENCE ################### */
2865/* ######################################################## */
2866
2867/* Initialize some droiddoc stuff, but only if we're in the reference */
2868if (location.pathname.indexOf("/reference") == 0) {
2869  if(!(location.pathname.indexOf("/reference-gms/packages.html") == 0)
2870    && !(location.pathname.indexOf("/reference-gcm/packages.html") == 0)
2871    && !(location.pathname.indexOf("/reference/com/google") == 0)) {
2872    $(document).ready(function() {
2873      // init available apis based on user pref
2874      changeApiLevel();
2875      initSidenavHeightResize()
2876      });
2877  }
2878}
2879
2880var API_LEVEL_COOKIE = "api_level";
2881var minLevel = 1;
2882var maxLevel = 1;
2883
2884/******* SIDENAV DIMENSIONS ************/
2885
2886  function initSidenavHeightResize() {
2887    // Change the drag bar size to nicely fit the scrollbar positions
2888    var $dragBar = $(".ui-resizable-s");
2889    $dragBar.css({'width': $dragBar.parent().width() - 5 + "px"});
2890
2891    $( "#resize-packages-nav" ).resizable({
2892      containment: "#nav-panels",
2893      handles: "s",
2894      alsoResize: "#packages-nav",
2895      resize: function(event, ui) { resizeNav(); }, /* resize the nav while dragging */
2896      stop: function(event, ui) { saveNavPanels(); } /* once stopped, save the sizes to cookie  */
2897      });
2898
2899  }
2900
2901function updateSidenavFixedWidth() {
2902  if (!sticky) return;
2903  $('#devdoc-nav').css({
2904    'width' : $('#side-nav').css('width'),
2905    'margin' : $('#side-nav').css('margin')
2906  });
2907  $('#devdoc-nav a.totop').css({'display':'block','width':$("#nav").innerWidth()+'px'});
2908
2909  initSidenavHeightResize();
2910}
2911
2912function updateSidenavFullscreenWidth() {
2913  if (!sticky) return;
2914  $('#devdoc-nav').css({
2915    'width' : $('#side-nav').css('width'),
2916    'margin' : $('#side-nav').css('margin')
2917  });
2918  $('#devdoc-nav .totop').css({'left': 'inherit'});
2919
2920  initSidenavHeightResize();
2921}
2922
2923function buildApiLevelSelector() {
2924  maxLevel = SINCE_DATA.length;
2925  var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE));
2926  userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default
2927
2928  minLevel = parseInt($("#doc-api-level").attr("class"));
2929  // Handle provisional api levels; the provisional level will always be the highest possible level
2930  // Provisional api levels will also have a length; other stuff that's just missing a level won't,
2931  // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class)
2932  if (isNaN(minLevel) && minLevel.length) {
2933    minLevel = maxLevel;
2934  }
2935  var select = $("#apiLevelSelector").html("").change(changeApiLevel);
2936  for (var i = maxLevel-1; i >= 0; i--) {
2937    var option = $("<option />").attr("value",""+SINCE_DATA[i]).append(""+SINCE_DATA[i]);
2938  //  if (SINCE_DATA[i] < minLevel) option.addClass("absent"); // always false for strings (codenames)
2939    select.append(option);
2940  }
2941
2942  // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true)
2943  var selectedLevelItem = $("#apiLevelSelector option[value='"+userApiLevel+"']").get(0);
2944  selectedLevelItem.setAttribute('selected',true);
2945}
2946
2947function changeApiLevel() {
2948  maxLevel = SINCE_DATA.length;
2949  var selectedLevel = maxLevel;
2950
2951  selectedLevel = parseInt($("#apiLevelSelector option:selected").val());
2952  toggleVisisbleApis(selectedLevel, "body");
2953
2954  writeCookie(API_LEVEL_COOKIE, selectedLevel, null);
2955
2956  if (selectedLevel < minLevel) {
2957    var thing = ($("#jd-header").html().indexOf("package") != -1) ? "package" : "class";
2958    $("#naMessage").show().html("<div><p><strong>This " + thing
2959              + " requires API level " + minLevel + " or higher.</strong></p>"
2960              + "<p>This document is hidden because your selected API level for the documentation is "
2961              + selectedLevel + ". You can change the documentation API level with the selector "
2962              + "above the left navigation.</p>"
2963              + "<p>For more information about specifying the API level your app requires, "
2964              + "read <a href='" + toRoot + "training/basics/supporting-devices/platforms.html'"
2965              + ">Supporting Different Platform Versions</a>.</p>"
2966              + "<input type='button' value='OK, make this page visible' "
2967              + "title='Change the API level to " + minLevel + "' "
2968              + "onclick='$(\"#apiLevelSelector\").val(\"" + minLevel + "\");changeApiLevel();' />"
2969              + "</div>");
2970  } else {
2971    $("#naMessage").hide();
2972  }
2973}
2974
2975function toggleVisisbleApis(selectedLevel, context) {
2976  var apis = $(".api",context);
2977  apis.each(function(i) {
2978    var obj = $(this);
2979    var className = obj.attr("class");
2980    var apiLevelIndex = className.lastIndexOf("-")+1;
2981    var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex);
2982    apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length;
2983    var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex);
2984    if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail
2985      return;
2986    }
2987    apiLevel = parseInt(apiLevel);
2988
2989    // Handle provisional api levels; if this item's level is the provisional one, set it to the max
2990    var selectedLevelNum = parseInt(selectedLevel)
2991    var apiLevelNum = parseInt(apiLevel);
2992    if (isNaN(apiLevelNum)) {
2993        apiLevelNum = maxLevel;
2994    }
2995
2996    // Grey things out that aren't available and give a tooltip title
2997    if (apiLevelNum > selectedLevelNum) {
2998      obj.addClass("absent").attr("title","Requires API Level \""
2999            + apiLevel + "\" or higher. To reveal, change the target API level "
3000              + "above the left navigation.");
3001    }
3002    else obj.removeClass("absent").removeAttr("title");
3003  });
3004}
3005
3006
3007
3008
3009/* #################  SIDENAV TREE VIEW ################### */
3010
3011function new_node(me, mom, text, link, children_data, api_level)
3012{
3013  var node = new Object();
3014  node.children = Array();
3015  node.children_data = children_data;
3016  node.depth = mom.depth + 1;
3017
3018  node.li = document.createElement("li");
3019  mom.get_children_ul().appendChild(node.li);
3020
3021  node.label_div = document.createElement("div");
3022  node.label_div.className = "label";
3023  if (api_level != null) {
3024    $(node.label_div).addClass("api");
3025    $(node.label_div).addClass("api-level-"+api_level);
3026  }
3027  node.li.appendChild(node.label_div);
3028
3029  if (children_data != null) {
3030    node.expand_toggle = document.createElement("a");
3031    node.expand_toggle.href = "javascript:void(0)";
3032    node.expand_toggle.onclick = function() {
3033          if (node.expanded) {
3034            $(node.get_children_ul()).slideUp("fast");
3035            node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png";
3036            node.expanded = false;
3037          } else {
3038            expand_node(me, node);
3039          }
3040       };
3041    node.label_div.appendChild(node.expand_toggle);
3042
3043    node.plus_img = document.createElement("img");
3044    node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png";
3045    node.plus_img.className = "plus";
3046    node.plus_img.width = "8";
3047    node.plus_img.border = "0";
3048    node.expand_toggle.appendChild(node.plus_img);
3049
3050    node.expanded = false;
3051  }
3052
3053  var a = document.createElement("a");
3054  node.label_div.appendChild(a);
3055  node.label = document.createTextNode(text);
3056  a.appendChild(node.label);
3057  if (link) {
3058    a.href = me.toroot + link;
3059  } else {
3060    if (children_data != null) {
3061      a.className = "nolink";
3062      a.href = "javascript:void(0)";
3063      a.onclick = node.expand_toggle.onclick;
3064      // This next line shouldn't be necessary.  I'll buy a beer for the first
3065      // person who figures out how to remove this line and have the link
3066      // toggle shut on the first try. --joeo@android.com
3067      node.expanded = false;
3068    }
3069  }
3070
3071
3072  node.children_ul = null;
3073  node.get_children_ul = function() {
3074      if (!node.children_ul) {
3075        node.children_ul = document.createElement("ul");
3076        node.children_ul.className = "children_ul";
3077        node.children_ul.style.display = "none";
3078        node.li.appendChild(node.children_ul);
3079      }
3080      return node.children_ul;
3081    };
3082
3083  return node;
3084}
3085
3086
3087
3088
3089function expand_node(me, node)
3090{
3091  if (node.children_data && !node.expanded) {
3092    if (node.children_visited) {
3093      $(node.get_children_ul()).slideDown("fast");
3094    } else {
3095      get_node(me, node);
3096      if ($(node.label_div).hasClass("absent")) {
3097        $(node.get_children_ul()).addClass("absent");
3098      }
3099      $(node.get_children_ul()).slideDown("fast");
3100    }
3101    node.plus_img.src = me.toroot + "assets/images/triangle-opened-small.png";
3102    node.expanded = true;
3103
3104    // perform api level toggling because new nodes are new to the DOM
3105    var selectedLevel = $("#apiLevelSelector option:selected").val();
3106    toggleVisisbleApis(selectedLevel, "#side-nav");
3107  }
3108}
3109
3110function get_node(me, mom)
3111{
3112  mom.children_visited = true;
3113  for (var i in mom.children_data) {
3114    var node_data = mom.children_data[i];
3115    mom.children[i] = new_node(me, mom, node_data[0], node_data[1],
3116        node_data[2], node_data[3]);
3117  }
3118}
3119
3120function this_page_relative(toroot)
3121{
3122  var full = document.location.pathname;
3123  var file = "";
3124  if (toroot.substr(0, 1) == "/") {
3125    if (full.substr(0, toroot.length) == toroot) {
3126      return full.substr(toroot.length);
3127    } else {
3128      // the file isn't under toroot.  Fail.
3129      return null;
3130    }
3131  } else {
3132    if (toroot != "./") {
3133      toroot = "./" + toroot;
3134    }
3135    do {
3136      if (toroot.substr(toroot.length-3, 3) == "../" || toroot == "./") {
3137        var pos = full.lastIndexOf("/");
3138        file = full.substr(pos) + file;
3139        full = full.substr(0, pos);
3140        toroot = toroot.substr(0, toroot.length-3);
3141      }
3142    } while (toroot != "" && toroot != "/");
3143    return file.substr(1);
3144  }
3145}
3146
3147function find_page(url, data)
3148{
3149  var nodes = data;
3150  var result = null;
3151  for (var i in nodes) {
3152    var d = nodes[i];
3153    if (d[1] == url) {
3154      return new Array(i);
3155    }
3156    else if (d[2] != null) {
3157      result = find_page(url, d[2]);
3158      if (result != null) {
3159        return (new Array(i).concat(result));
3160      }
3161    }
3162  }
3163  return null;
3164}
3165
3166function init_default_navtree(toroot) {
3167  // load json file for navtree data
3168  $.getScript(toRoot + 'navtree_data.js', function(data, textStatus, jqxhr) {
3169      // when the file is loaded, initialize the tree
3170      if(jqxhr.status === 200) {
3171          init_navtree("tree-list", toroot, NAVTREE_DATA);
3172      }
3173  });
3174
3175  // perform api level toggling because because the whole tree is new to the DOM
3176  var selectedLevel = $("#apiLevelSelector option:selected").val();
3177  toggleVisisbleApis(selectedLevel, "#side-nav");
3178}
3179
3180function init_navtree(navtree_id, toroot, root_nodes)
3181{
3182  var me = new Object();
3183  me.toroot = toroot;
3184  me.node = new Object();
3185
3186  me.node.li = document.getElementById(navtree_id);
3187  me.node.children_data = root_nodes;
3188  me.node.children = new Array();
3189  me.node.children_ul = document.createElement("ul");
3190  me.node.get_children_ul = function() { return me.node.children_ul; };
3191  //me.node.children_ul.className = "children_ul";
3192  me.node.li.appendChild(me.node.children_ul);
3193  me.node.depth = 0;
3194
3195  get_node(me, me.node);
3196
3197  me.this_page = this_page_relative(toroot);
3198  me.breadcrumbs = find_page(me.this_page, root_nodes);
3199  if (me.breadcrumbs != null && me.breadcrumbs.length != 0) {
3200    var mom = me.node;
3201    for (var i in me.breadcrumbs) {
3202      var j = me.breadcrumbs[i];
3203      mom = mom.children[j];
3204      expand_node(me, mom);
3205    }
3206    mom.label_div.className = mom.label_div.className + " selected";
3207    addLoadEvent(function() {
3208      scrollIntoView("nav-tree");
3209      });
3210  }
3211}
3212
3213
3214
3215
3216
3217
3218
3219
3220/* TODO: eliminate redundancy with non-google functions */
3221function init_google_navtree(navtree_id, toroot, root_nodes)
3222{
3223  var me = new Object();
3224  me.toroot = toroot;
3225  me.node = new Object();
3226
3227  me.node.li = document.getElementById(navtree_id);
3228  if (!me.node.li) {
3229    return;
3230  }
3231
3232  me.node.children_data = root_nodes;
3233  me.node.children = new Array();
3234  me.node.children_ul = document.createElement("ul");
3235  me.node.get_children_ul = function() { return me.node.children_ul; };
3236  //me.node.children_ul.className = "children_ul";
3237  me.node.li.appendChild(me.node.children_ul);
3238  me.node.depth = 0;
3239
3240  get_google_node(me, me.node);
3241}
3242
3243function new_google_node(me, mom, text, link, children_data, api_level)
3244{
3245  var node = new Object();
3246  var child;
3247  node.children = Array();
3248  node.children_data = children_data;
3249  node.depth = mom.depth + 1;
3250  node.get_children_ul = function() {
3251      if (!node.children_ul) {
3252        node.children_ul = document.createElement("ul");
3253        node.children_ul.className = "tree-list-children";
3254        node.li.appendChild(node.children_ul);
3255      }
3256      return node.children_ul;
3257    };
3258  node.li = document.createElement("li");
3259
3260  mom.get_children_ul().appendChild(node.li);
3261
3262
3263  if(link) {
3264    child = document.createElement("a");
3265
3266  }
3267  else {
3268    child = document.createElement("span");
3269    child.className = "tree-list-subtitle";
3270
3271  }
3272  if (children_data != null) {
3273    node.li.className="nav-section";
3274    node.label_div = document.createElement("div");
3275    node.label_div.className = "nav-section-header-ref";
3276    node.li.appendChild(node.label_div);
3277    get_google_node(me, node);
3278    node.label_div.appendChild(child);
3279  }
3280  else {
3281    node.li.appendChild(child);
3282  }
3283  if(link) {
3284    child.href = me.toroot + link;
3285  }
3286  node.label = document.createTextNode(text);
3287  child.appendChild(node.label);
3288
3289  node.children_ul = null;
3290
3291  return node;
3292}
3293
3294function get_google_node(me, mom)
3295{
3296  mom.children_visited = true;
3297  var linkText;
3298  for (var i in mom.children_data) {
3299    var node_data = mom.children_data[i];
3300    linkText = node_data[0];
3301
3302    if(linkText.match("^"+"com.google.android")=="com.google.android"){
3303      linkText = linkText.substr(19, linkText.length);
3304    }
3305      mom.children[i] = new_google_node(me, mom, linkText, node_data[1],
3306          node_data[2], node_data[3]);
3307  }
3308}
3309
3310
3311
3312
3313
3314
3315/****** NEW version of script to build google and sample navs dynamically ******/
3316// TODO: update Google reference docs to tolerate this new implementation
3317
3318var NODE_NAME = 0;
3319var NODE_HREF = 1;
3320var NODE_GROUP = 2;
3321var NODE_TAGS = 3;
3322var NODE_CHILDREN = 4;
3323
3324function init_google_navtree2(navtree_id, data)
3325{
3326  var $containerUl = $("#"+navtree_id);
3327  for (var i in data) {
3328    var node_data = data[i];
3329    $containerUl.append(new_google_node2(node_data));
3330  }
3331
3332  // Make all third-generation list items 'sticky' to prevent them from collapsing
3333  $containerUl.find('li li li.nav-section').addClass('sticky');
3334
3335  initExpandableNavItems("#"+navtree_id);
3336}
3337
3338function new_google_node2(node_data)
3339{
3340  var linkText = node_data[NODE_NAME];
3341  if(linkText.match("^"+"com.google.android")=="com.google.android"){
3342    linkText = linkText.substr(19, linkText.length);
3343  }
3344  var $li = $('<li>');
3345  var $a;
3346  if (node_data[NODE_HREF] != null) {
3347    $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >'
3348        + linkText + '</a>');
3349  } else {
3350    $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >'
3351        + linkText + '/</a>');
3352  }
3353  var $childUl = $('<ul>');
3354  if (node_data[NODE_CHILDREN] != null) {
3355    $li.addClass("nav-section");
3356    $a = $('<div class="nav-section-header">').append($a);
3357    if (node_data[NODE_HREF] == null) $a.addClass('empty');
3358
3359    for (var i in node_data[NODE_CHILDREN]) {
3360      var child_node_data = node_data[NODE_CHILDREN][i];
3361      $childUl.append(new_google_node2(child_node_data));
3362    }
3363    $li.append($childUl);
3364  }
3365  $li.prepend($a);
3366
3367  return $li;
3368}
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380function showGoogleRefTree() {
3381  init_default_google_navtree(toRoot);
3382  init_default_gcm_navtree(toRoot);
3383}
3384
3385function init_default_google_navtree(toroot) {
3386  // load json file for navtree data
3387  $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) {
3388      // when the file is loaded, initialize the tree
3389      if(jqxhr.status === 200) {
3390          init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA);
3391          highlightSidenav();
3392          resizeNav();
3393      }
3394  });
3395}
3396
3397function init_default_gcm_navtree(toroot) {
3398  // load json file for navtree data
3399  $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) {
3400      // when the file is loaded, initialize the tree
3401      if(jqxhr.status === 200) {
3402          init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA);
3403          highlightSidenav();
3404          resizeNav();
3405      }
3406  });
3407}
3408
3409function showSamplesRefTree() {
3410  init_default_samples_navtree(toRoot);
3411}
3412
3413function init_default_samples_navtree(toroot) {
3414  // load json file for navtree data
3415  $.getScript(toRoot + 'samples_navtree_data.js', function(data, textStatus, jqxhr) {
3416      // when the file is loaded, initialize the tree
3417      if(jqxhr.status === 200) {
3418          // hack to remove the "about the samples" link then put it back in
3419          // after we nuke the list to remove the dummy static list of samples
3420          var $firstLi = $("#nav.samples-nav > li:first-child").clone();
3421          $("#nav.samples-nav").empty();
3422          $("#nav.samples-nav").append($firstLi);
3423
3424          init_google_navtree2("nav.samples-nav", SAMPLES_NAVTREE_DATA);
3425          highlightSidenav();
3426          resizeNav();
3427          if ($("#jd-content #samples").length) {
3428            showSamples();
3429          }
3430      }
3431  });
3432}
3433
3434/* TOGGLE INHERITED MEMBERS */
3435
3436/* Toggle an inherited class (arrow toggle)
3437 * @param linkObj  The link that was clicked.
3438 * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
3439 *                'null' to simply toggle.
3440 */
3441function toggleInherited(linkObj, expand) {
3442    var base = linkObj.getAttribute("id");
3443    var list = document.getElementById(base + "-list");
3444    var summary = document.getElementById(base + "-summary");
3445    var trigger = document.getElementById(base + "-trigger");
3446    var a = $(linkObj);
3447    if ( (expand == null && a.hasClass("closed")) || expand ) {
3448        list.style.display = "none";
3449        summary.style.display = "block";
3450        trigger.src = toRoot + "assets/images/styles/disclosure_up.png";
3451        a.removeClass("closed");
3452        a.addClass("opened");
3453    } else if ( (expand == null && a.hasClass("opened")) || (expand == false) ) {
3454        list.style.display = "block";
3455        summary.style.display = "none";
3456        trigger.src = toRoot + "assets/images/styles/disclosure_down.png";
3457        a.removeClass("opened");
3458        a.addClass("closed");
3459    }
3460    return false;
3461}
3462
3463/* Toggle all inherited classes in a single table (e.g. all inherited methods)
3464 * @param linkObj  The link that was clicked.
3465 * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
3466 *                'null' to simply toggle.
3467 */
3468function toggleAllInherited(linkObj, expand) {
3469  var a = $(linkObj);
3470  var table = $(a.parent().parent().parent()); // ugly way to get table/tbody
3471  var expandos = $(".jd-expando-trigger", table);
3472  if ( (expand == null && a.text() == "[Expand]") || expand ) {
3473    expandos.each(function(i) {
3474      toggleInherited(this, true);
3475    });
3476    a.text("[Collapse]");
3477  } else if ( (expand == null && a.text() == "[Collapse]") || (expand == false) ) {
3478    expandos.each(function(i) {
3479      toggleInherited(this, false);
3480    });
3481    a.text("[Expand]");
3482  }
3483  return false;
3484}
3485
3486/* Toggle all inherited members in the class (link in the class title)
3487 */
3488function toggleAllClassInherited() {
3489  var a = $("#toggleAllClassInherited"); // get toggle link from class title
3490  var toggles = $(".toggle-all", $("#body-content"));
3491  if (a.text() == "[Expand All]") {
3492    toggles.each(function(i) {
3493      toggleAllInherited(this, true);
3494    });
3495    a.text("[Collapse All]");
3496  } else {
3497    toggles.each(function(i) {
3498      toggleAllInherited(this, false);
3499    });
3500    a.text("[Expand All]");
3501  }
3502  return false;
3503}
3504
3505/* Expand all inherited members in the class. Used when initiating page search */
3506function ensureAllInheritedExpanded() {
3507  var toggles = $(".toggle-all", $("#body-content"));
3508  toggles.each(function(i) {
3509    toggleAllInherited(this, true);
3510  });
3511  $("#toggleAllClassInherited").text("[Collapse All]");
3512}
3513
3514
3515/* HANDLE KEY EVENTS
3516 * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search)
3517 */
3518var agent = navigator['userAgent'].toLowerCase();
3519var mac = agent.indexOf("macintosh") != -1;
3520
3521$(document).keydown( function(e) {
3522var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key
3523  if (control && e.which == 70) {  // 70 is "F"
3524    ensureAllInheritedExpanded();
3525  }
3526});
3527
3528
3529
3530
3531
3532
3533/* On-demand functions */
3534
3535/** Move sample code line numbers out of PRE block and into non-copyable column */
3536function initCodeLineNumbers() {
3537  var numbers = $("#codesample-block a.number");
3538  if (numbers.length) {
3539    $("#codesample-line-numbers").removeClass("hidden").append(numbers);
3540  }
3541
3542  $(document).ready(function() {
3543    // select entire line when clicked
3544    $("span.code-line").click(function() {
3545      if (!shifted) {
3546        selectText(this);
3547      }
3548    });
3549    // invoke line link on double click
3550    $(".code-line").dblclick(function() {
3551      document.location.hash = $(this).attr('id');
3552    });
3553    // highlight the line when hovering on the number
3554    $("#codesample-line-numbers a.number").mouseover(function() {
3555      var id = $(this).attr('href');
3556      $(id).css('background','#e7e7e7');
3557    });
3558    $("#codesample-line-numbers a.number").mouseout(function() {
3559      var id = $(this).attr('href');
3560      $(id).css('background','none');
3561    });
3562  });
3563}
3564
3565// create SHIFT key binder to avoid the selectText method when selecting multiple lines
3566var shifted = false;
3567$(document).bind('keyup keydown', function(e){shifted = e.shiftKey; return true;} );
3568
3569// courtesy of jasonedelman.com
3570function selectText(element) {
3571    var doc = document
3572        , range, selection
3573    ;
3574    if (doc.body.createTextRange) { //ms
3575        range = doc.body.createTextRange();
3576        range.moveToElementText(element);
3577        range.select();
3578    } else if (window.getSelection) { //all others
3579        selection = window.getSelection();
3580        range = doc.createRange();
3581        range.selectNodeContents(element);
3582        selection.removeAllRanges();
3583        selection.addRange(range);
3584    }
3585}
3586
3587
3588
3589
3590/** Display links and other information about samples that match the
3591    group specified by the URL */
3592function showSamples() {
3593  var group = $("#samples").attr('class');
3594  $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>");
3595
3596  var $ul = $("<ul>");
3597  $selectedLi = $("#nav li.selected");
3598
3599  $selectedLi.children("ul").children("li").each(function() {
3600      var $li = $("<li>").append($(this).find("a").first().clone());
3601      $ul.append($li);
3602  });
3603
3604  $("#samples").append($ul);
3605
3606}
3607
3608
3609
3610/* ########################################################## */
3611/* ###################  RESOURCE CARDS  ##################### */
3612/* ########################################################## */
3613
3614/** Handle resource queries, collections, and grids (sections). Requires
3615    jd_tag_helpers.js and the *_unified_data.js to be loaded. */
3616
3617(function() {
3618  // Prevent the same resource from being loaded more than once per page.
3619  var addedPageResources = {};
3620
3621  $(document).ready(function() {
3622    // Need to initialize hero carousel before other sections for dedupe
3623    // to work correctly.
3624    $('[data-carousel-query]').dacCarouselQuery();
3625
3626    $('.resource-widget').each(function() {
3627      initResourceWidget(this);
3628    });
3629
3630    /* Pass the line height to ellipsisfade() to adjust the height of the
3631    text container to show the max number of lines possible, without
3632    showing lines that are cut off. This works with the css ellipsis
3633    classes to fade last text line and apply an ellipsis char. */
3634
3635    //card text currently uses 20px line height.
3636    var lineHeight = 20;
3637    $('.card-info .text').ellipsisfade(lineHeight);
3638  });
3639
3640  /*
3641    Three types of resource layouts:
3642    Flow - Uses a fixed row-height flow using float left style.
3643    Carousel - Single card slideshow all same dimension absolute.
3644    Stack - Uses fixed columns and flexible element height.
3645  */
3646  function initResourceWidget(widget) {
3647    var $widget = $(widget);
3648    var isFlow = $widget.hasClass('resource-flow-layout'),
3649        isCarousel = $widget.hasClass('resource-carousel-layout'),
3650        isStack = $widget.hasClass('resource-stack-layout');
3651
3652    // remove illegal col-x class which is not relevant anymore thanks to responsive styles.
3653    var m = $widget.get(0).className.match(/\bcol-(\d+)\b/);
3654    if (m && !$widget.is('.cols > *')) {
3655      $widget.removeClass('col-' + m[1]);
3656    }
3657
3658    var opts = {
3659      cardSizes: ($widget.data('cardsizes') || '').split(','),
3660      maxResults: parseInt($widget.data('maxresults') || '100', 10),
3661      initialResults: $widget.data('initialResults'),
3662      itemsPerPage: $widget.data('itemsperpage'),
3663      sortOrder: $widget.data('sortorder'),
3664      query: $widget.data('query'),
3665      section: $widget.data('section'),
3666      /* Added by LFL 6/6/14 */
3667      resourceStyle: $widget.data('resourcestyle') || 'card',
3668      stackSort: $widget.data('stacksort') || 'true'
3669    };
3670
3671    // run the search for the set of resources to show
3672
3673    var resources = buildResourceList(opts);
3674
3675    if (isFlow) {
3676      drawResourcesFlowWidget($widget, opts, resources);
3677    } else if (isCarousel) {
3678      drawResourcesCarouselWidget($widget, opts, resources);
3679    } else if (isStack) {
3680      /* Looks like this got removed and is not used, so repurposing for the
3681          homepage style layout.
3682          Modified by LFL 6/6/14
3683      */
3684      //var sections = buildSectionList(opts);
3685      opts['numStacks'] = $widget.data('numstacks');
3686      drawResourcesStackWidget($widget, opts, resources/*, sections*/);
3687    }
3688  }
3689
3690  /* Initializes a Resource Carousel Widget */
3691  function drawResourcesCarouselWidget($widget, opts, resources) {
3692    $widget.empty();
3693    var plusone = false; // stop showing plusone buttons on cards
3694
3695    $widget.addClass('resource-card slideshow-container')
3696      .append($('<a>').addClass('slideshow-prev').text('Prev'))
3697      .append($('<a>').addClass('slideshow-next').text('Next'));
3698
3699    var css = { 'width': $widget.width() + 'px',
3700                'height': $widget.height() + 'px' };
3701
3702    var $ul = $('<ul>');
3703
3704    for (var i = 0; i < resources.length; ++i) {
3705      var $card = $('<a>')
3706        .attr('href', cleanUrl(resources[i].url))
3707        .decorateResourceCard(resources[i],plusone);
3708
3709      $('<li>').css(css)
3710          .append($card)
3711          .appendTo($ul);
3712    }
3713
3714    $('<div>').addClass('frame')
3715      .append($ul)
3716      .appendTo($widget);
3717
3718    $widget.dacSlideshow({
3719      auto: true,
3720      btnPrev: '.slideshow-prev',
3721      btnNext: '.slideshow-next'
3722    });
3723  };
3724
3725  /* Initializes a Resource Card Stack Widget (column-based layout)
3726     Modified by LFL 6/6/14
3727   */
3728  function drawResourcesStackWidget($widget, opts, resources, sections) {
3729    // Don't empty widget, grab all items inside since they will be the first
3730    // items stacked, followed by the resource query
3731    var plusone = false; // stop showing plusone buttons on cards
3732    var cards = $widget.find('.resource-card').detach().toArray();
3733    var numStacks = opts.numStacks || 1;
3734    var $stacks = [];
3735    var urlString;
3736
3737    for (var i = 0; i < numStacks; ++i) {
3738      $stacks[i] = $('<div>').addClass('resource-card-stack')
3739          .appendTo($widget);
3740    }
3741
3742    var sectionResources = [];
3743
3744    // Extract any subsections that are actually resource cards
3745    if (sections) {
3746      for (var i = 0; i < sections.length; ++i) {
3747        if (!sections[i].sections || !sections[i].sections.length) {
3748          // Render it as a resource card
3749          sectionResources.push(
3750            $('<a>')
3751              .addClass('resource-card section-card')
3752              .attr('href', cleanUrl(sections[i].resource.url))
3753              .decorateResourceCard(sections[i].resource,plusone)[0]
3754          );
3755
3756        } else {
3757          cards.push(
3758            $('<div>')
3759              .addClass('resource-card section-card-menu')
3760              .decorateResourceSection(sections[i],plusone)[0]
3761          );
3762        }
3763      }
3764    }
3765
3766    cards = cards.concat(sectionResources);
3767
3768    for (var i = 0; i < resources.length; ++i) {
3769      var $card = createResourceElement(resources[i], opts);
3770
3771      if (opts.resourceStyle.indexOf('related') > -1) {
3772        $card.addClass('related-card');
3773      }
3774
3775      cards.push($card[0]);
3776    }
3777
3778    if (opts.stackSort != 'false') {
3779      for (var i = 0; i < cards.length; ++i) {
3780        // Find the stack with the shortest height, but give preference to
3781        // left to right order.
3782        var minHeight = $stacks[0].height();
3783        var minIndex = 0;
3784
3785        for (var j = 1; j < numStacks; ++j) {
3786          var height = $stacks[j].height();
3787          if (height < minHeight - 45) {
3788            minHeight = height;
3789            minIndex = j;
3790          }
3791        }
3792
3793        $stacks[minIndex].append($(cards[i]));
3794      }
3795    }
3796
3797  };
3798
3799  /*
3800    Create a resource card using the given resource object and a list of html
3801     configured options. Returns a jquery object containing the element.
3802  */
3803  function createResourceElement(resource, opts, plusone) {
3804    var $el;
3805
3806    // The difference here is that generic cards are not entirely clickable
3807    // so its a div instead of an a tag, also the generic one is not given
3808    // the resource-card class so it appears with a transparent background
3809    // and can be styled in whatever way the css setup.
3810    if (opts.resourceStyle == 'generic') {
3811      $el = $('<div>')
3812        .addClass('resource')
3813        .attr('href', cleanUrl(resource.url))
3814        .decorateResource(resource, opts);
3815    } else {
3816      var cls = 'resource resource-card';
3817
3818      $el = $('<a>')
3819        .addClass(cls)
3820        .attr('href', cleanUrl(resource.url))
3821        .decorateResourceCard(resource, plusone);
3822    }
3823
3824    return $el;
3825  }
3826
3827  function createResponsiveFlowColumn(cardSize) {
3828    var cardWidth = parseInt(cardSize.match(/(\d+)/)[1], 10);
3829    var column = $('<div>').addClass('col-' + (cardWidth / 3) + 'of6');
3830    if (cardWidth < 9) {
3831      column.addClass('col-tablet-1of2');
3832    } else if (cardWidth > 9 && cardWidth < 18) {
3833      column.addClass('col-tablet-1of1');
3834    }
3835    if (cardWidth < 18) {
3836      column.addClass('col-mobile-1of1')
3837    }
3838    return column;
3839  }
3840
3841  /* Initializes a flow widget, see distribute.scss for generating accompanying css */
3842  function drawResourcesFlowWidget($widget, opts, resources) {
3843    $widget.empty().addClass('cols');
3844    var cardSizes = opts.cardSizes || ['6x6'];
3845    var initialResults = opts.initialResults || resources.length;
3846    var i = 0, j = 0;
3847    var plusone = false; // stop showing plusone buttons on cards
3848    var cardParent = $widget;
3849
3850    while (i < resources.length) {
3851
3852      if (i === initialResults && initialResults < resources.length) {
3853        // Toggle remaining cards
3854        cardParent = $('<div class="dac-toggle-content clearfix">').appendTo($widget);
3855        $widget.addClass('dac-toggle');
3856        $('<div class="col-1of1 dac-section-links dac-text-center">')
3857          .append(
3858            $('<div class="dac-section-link" data-toggle="section">')
3859              .append('<span class="dac-toggle-expand">More<i class="dac-sprite dac-auto-unfold-more"></i></span>')
3860              .append('<span class="dac-toggle-collapse">Less<i class="dac-sprite dac-auto-unfold-less"></i></span>')
3861          )
3862          .appendTo($widget)
3863      }
3864
3865      var cardSize = cardSizes[j++ % cardSizes.length];
3866      cardSize = cardSize.replace(/^\s+|\s+$/,'');
3867
3868      var column = createResponsiveFlowColumn(cardSize).appendTo(cardParent);
3869
3870      // A stack has a third dimension which is the number of stacked items
3871      var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/);
3872      var stackCount = 0;
3873      var $stackDiv = null;
3874
3875      if (isStack) {
3876        // Create a stack container which should have the dimensions defined
3877        // by the product of the items inside.
3878        $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1]
3879            + 'x' + isStack[2] * isStack[3]) .appendTo(column);
3880      }
3881
3882      // Build each stack item or just a single item
3883      do {
3884        var resource = resources[i];
3885
3886        var $card = createResourceElement(resources[i], opts, plusone);
3887
3888        $card.addClass('resource-card-' + cardSize +
3889          ' resource-card-' + resource.type);
3890
3891        if (isStack) {
3892          $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]);
3893          if (++stackCount == parseInt(isStack[3])) {
3894            $card.addClass('resource-card-row-stack-last');
3895            stackCount = 0;
3896          }
3897        } else {
3898          stackCount = 0;
3899        }
3900
3901        $card.appendTo($stackDiv || column);
3902
3903      } while (++i < resources.length && stackCount > 0);
3904    }
3905  }
3906
3907  /* Build a site map of resources using a section as a root. */
3908  function buildSectionList(opts) {
3909    if (opts.section && SECTION_BY_ID[opts.section]) {
3910      return SECTION_BY_ID[opts.section].sections || [];
3911    }
3912    return [];
3913  }
3914
3915  function buildResourceList(opts) {
3916    return $.queryResources(opts);
3917  }
3918
3919  $.queryResources = function(opts) {
3920    var maxResults = opts.maxResults || 100;
3921
3922    var query = opts.query || '';
3923    var expressions = parseResourceQuery(query);
3924    var addedResourceIndices = {};
3925    var results = [];
3926
3927    for (var i = 0; i < expressions.length; i++) {
3928      var clauses = expressions[i];
3929
3930      // build initial set of resources from first clause
3931      var firstClause = clauses[0];
3932      var resources = [];
3933      switch (firstClause.attr) {
3934        case 'type':
3935          resources = ALL_RESOURCES_BY_TYPE[firstClause.value];
3936          break;
3937        case 'lang':
3938          resources = ALL_RESOURCES_BY_LANG[firstClause.value];
3939          break;
3940        case 'tag':
3941          resources = ALL_RESOURCES_BY_TAG[firstClause.value];
3942          break;
3943        case 'collection':
3944          var urls = RESOURCE_COLLECTIONS[firstClause.value].resources || [];
3945          resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; });
3946          break;
3947        case 'section':
3948          var urls = SITE_MAP[firstClause.value].sections || [];
3949          resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; });
3950          break;
3951      }
3952      // console.log(firstClause.attr + ':' + firstClause.value);
3953      resources = resources || [];
3954
3955      // use additional clauses to filter corpus
3956      if (clauses.length > 1) {
3957        var otherClauses = clauses.slice(1);
3958        resources = resources.filter(getResourceMatchesClausesFilter(otherClauses));
3959      }
3960
3961      // filter out resources already added
3962      if (i > 1) {
3963        resources = resources.filter(getResourceNotAlreadyAddedFilter(addedResourceIndices));
3964      }
3965
3966      // add to list of already added indices
3967      for (var j = 0; j < resources.length; j++) {
3968        if (resources[j]) {
3969          addedResourceIndices[resources[j].index] = 1;
3970        }
3971      }
3972
3973      // concat to final results list
3974      results = results.concat(resources);
3975    }
3976
3977    if (opts.sortOrder && results.length) {
3978      var attr = opts.sortOrder;
3979
3980      if (opts.sortOrder == 'random') {
3981        var i = results.length, j, temp;
3982        while (--i) {
3983          j = Math.floor(Math.random() * (i + 1));
3984          temp = results[i];
3985          results[i] = results[j];
3986          results[j] = temp;
3987        }
3988      } else {
3989        var desc = attr.charAt(0) == '-';
3990        if (desc) {
3991          attr = attr.substring(1);
3992        }
3993        results = results.sort(function(x,y) {
3994          return (desc ? -1 : 1) * (parseInt(x[attr], 10) - parseInt(y[attr], 10));
3995        });
3996      }
3997    }
3998
3999    results = results.filter(getResourceNotAlreadyAddedFilter(addedPageResources));
4000    results = results.slice(0, maxResults);
4001
4002    for (var j = 0; j < results.length; ++j) {
4003      addedPageResources[results[j].index] = 1;
4004    }
4005
4006    return results;
4007  }
4008
4009
4010  function getResourceNotAlreadyAddedFilter(addedResourceIndices) {
4011    return function(resource) {
4012      return resource && !addedResourceIndices[resource.index];
4013    };
4014  }
4015
4016
4017  function getResourceMatchesClausesFilter(clauses) {
4018    return function(resource) {
4019      return doesResourceMatchClauses(resource, clauses);
4020    };
4021  }
4022
4023
4024  function doesResourceMatchClauses(resource, clauses) {
4025    for (var i = 0; i < clauses.length; i++) {
4026      var map;
4027      switch (clauses[i].attr) {
4028        case 'type':
4029          map = IS_RESOURCE_OF_TYPE[clauses[i].value];
4030          break;
4031        case 'lang':
4032          map = IS_RESOURCE_IN_LANG[clauses[i].value];
4033          break;
4034        case 'tag':
4035          map = IS_RESOURCE_TAGGED[clauses[i].value];
4036          break;
4037      }
4038
4039      if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) {
4040        return clauses[i].negative;
4041      }
4042    }
4043    return true;
4044  }
4045
4046  function cleanUrl(url)
4047  {
4048    if (url && url.indexOf('//') === -1) {
4049      url = toRoot + url;
4050    }
4051
4052    return url;
4053  }
4054
4055
4056  function parseResourceQuery(query) {
4057    // Parse query into array of expressions (expression e.g. 'tag:foo + type:video')
4058    var expressions = [];
4059    var expressionStrs = query.split(',') || [];
4060    for (var i = 0; i < expressionStrs.length; i++) {
4061      var expr = expressionStrs[i] || '';
4062
4063      // Break expression into clauses (clause e.g. 'tag:foo')
4064      var clauses = [];
4065      var clauseStrs = expr.split(/(?=[\+\-])/);
4066      for (var j = 0; j < clauseStrs.length; j++) {
4067        var clauseStr = clauseStrs[j] || '';
4068
4069        // Get attribute and value from clause (e.g. attribute='tag', value='foo')
4070        var parts = clauseStr.split(':');
4071        var clause = {};
4072
4073        clause.attr = parts[0].replace(/^\s+|\s+$/g,'');
4074        if (clause.attr) {
4075          if (clause.attr.charAt(0) == '+') {
4076            clause.attr = clause.attr.substring(1);
4077          } else if (clause.attr.charAt(0) == '-') {
4078            clause.negative = true;
4079            clause.attr = clause.attr.substring(1);
4080          }
4081        }
4082
4083        if (parts.length > 1) {
4084          clause.value = parts[1].replace(/^\s+|\s+$/g,'');
4085        }
4086
4087        clauses.push(clause);
4088      }
4089
4090      if (!clauses.length) {
4091        continue;
4092      }
4093
4094      expressions.push(clauses);
4095    }
4096
4097    return expressions;
4098  }
4099})();
4100
4101(function($) {
4102
4103  /*
4104    Utility method for creating dom for the description area of a card.
4105    Used in decorateResourceCard and decorateResource.
4106  */
4107  function buildResourceCardDescription(resource, plusone) {
4108    var $description = $('<div>').addClass('description ellipsis');
4109
4110    $description.append($('<div>').addClass('text').html(resource.summary));
4111
4112    if (resource.cta) {
4113      $description.append($('<a>').addClass('cta').html(resource.cta));
4114    }
4115
4116    if (plusone) {
4117      var plusurl = resource.url.indexOf("//") > -1 ? resource.url :
4118        "//developer.android.com/" + resource.url;
4119
4120      $description.append($('<div>').addClass('util')
4121        .append($('<div>').addClass('g-plusone')
4122          .attr('data-size', 'small')
4123          .attr('data-align', 'right')
4124          .attr('data-href', plusurl)));
4125    }
4126
4127    return $description;
4128  }
4129
4130
4131  /* Simple jquery function to create dom for a standard resource card */
4132  $.fn.decorateResourceCard = function(resource,plusone) {
4133    var section = resource.group || resource.type;
4134    var imgUrl = resource.image ||
4135      'assets/images/resource-card-default-android.jpg';
4136
4137    if (imgUrl.indexOf('//') === -1) {
4138      imgUrl = toRoot + imgUrl;
4139    }
4140
4141    if (resource.type === 'youtube') {
4142      $('<div>').addClass('play-button')
4143        .append($('<i class="dac-sprite dac-play-white">'))
4144        .appendTo(this);
4145    }
4146
4147    $('<div>').addClass('card-bg')
4148      .css('background-image', 'url(' + (imgUrl || toRoot +
4149        'assets/images/resource-card-default-android.jpg') + ')')
4150      .appendTo(this);
4151
4152    $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : ''))
4153      .append($('<div>').addClass('section').text(section))
4154      .append($('<div>').addClass('title').html(resource.title))
4155      .append(buildResourceCardDescription(resource, plusone))
4156      .appendTo(this);
4157
4158    return this;
4159  };
4160
4161  /* Simple jquery function to create dom for a resource section card (menu) */
4162  $.fn.decorateResourceSection = function(section,plusone) {
4163    var resource = section.resource;
4164    //keep url clean for matching and offline mode handling
4165    var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot;
4166    var $base = $('<a>')
4167        .addClass('card-bg')
4168        .attr('href', resource.url)
4169        .append($('<div>').addClass('card-section-icon')
4170          .append($('<div>').addClass('icon'))
4171          .append($('<div>').addClass('section').html(resource.title)))
4172      .appendTo(this);
4173
4174    var $cardInfo = $('<div>').addClass('card-info').appendTo(this);
4175
4176    if (section.sections && section.sections.length) {
4177      // Recurse the section sub-tree to find a resource image.
4178      var stack = [section];
4179
4180      while (stack.length) {
4181        if (stack[0].resource.image) {
4182          $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')');
4183          break;
4184        }
4185
4186        if (stack[0].sections) {
4187          stack = stack.concat(stack[0].sections);
4188        }
4189
4190        stack.shift();
4191      }
4192
4193      var $ul = $('<ul>')
4194        .appendTo($cardInfo);
4195
4196      var max = section.sections.length > 3 ? 3 : section.sections.length;
4197
4198      for (var i = 0; i < max; ++i) {
4199
4200        var subResource = section.sections[i];
4201        if (!plusone) {
4202          $('<li>')
4203            .append($('<a>').attr('href', subResource.url)
4204              .append($('<div>').addClass('title').html(subResource.title))
4205              .append($('<div>').addClass('description ellipsis')
4206                .append($('<div>').addClass('text').html(subResource.summary))
4207                .append($('<div>').addClass('util'))))
4208          .appendTo($ul);
4209        } else {
4210          $('<li>')
4211            .append($('<a>').attr('href', subResource.url)
4212              .append($('<div>').addClass('title').html(subResource.title))
4213              .append($('<div>').addClass('description ellipsis')
4214                .append($('<div>').addClass('text').html(subResource.summary))
4215                .append($('<div>').addClass('util')
4216                  .append($('<div>').addClass('g-plusone')
4217                    .attr('data-size', 'small')
4218                    .attr('data-align', 'right')
4219                    .attr('data-href', resource.url)))))
4220          .appendTo($ul);
4221        }
4222      }
4223
4224      // Add a more row
4225      if (max < section.sections.length) {
4226        $('<li>')
4227          .append($('<a>').attr('href', resource.url)
4228            .append($('<div>')
4229              .addClass('title')
4230              .text('More')))
4231        .appendTo($ul);
4232      }
4233    } else {
4234      // No sub-resources, just render description?
4235    }
4236
4237    return this;
4238  };
4239
4240
4241
4242
4243  /* Render other types of resource styles that are not cards. */
4244  $.fn.decorateResource = function(resource, opts) {
4245    var imgUrl = resource.image ||
4246      'assets/images/resource-card-default-android.jpg';
4247    var linkUrl = resource.url;
4248
4249    if (imgUrl.indexOf('//') === -1) {
4250      imgUrl = toRoot + imgUrl;
4251    }
4252
4253    if (linkUrl && linkUrl.indexOf('//') === -1) {
4254      linkUrl = toRoot + linkUrl;
4255    }
4256
4257    $(this).append(
4258      $('<div>').addClass('image')
4259        .css('background-image', 'url(' + imgUrl + ')'),
4260      $('<div>').addClass('info').append(
4261        $('<h4>').addClass('title').html(resource.title),
4262        $('<p>').addClass('summary').html(resource.summary),
4263        $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More')
4264      )
4265    );
4266
4267    return this;
4268  };
4269})(jQuery);
4270
4271
4272/* Calculate the vertical area remaining */
4273(function($) {
4274    $.fn.ellipsisfade= function(lineHeight) {
4275        this.each(function() {
4276            // get element text
4277            var $this = $(this);
4278            var remainingHeight = $this.parent().parent().height();
4279            $this.parent().siblings().each(function ()
4280            {
4281              if ($(this).is(":visible")) {
4282                var h = $(this).outerHeight(true);
4283                remainingHeight = remainingHeight - h;
4284              }
4285            });
4286
4287            adjustedRemainingHeight = ((remainingHeight)/lineHeight>>0)*lineHeight
4288            $this.parent().css({'height': adjustedRemainingHeight});
4289            $this.css({'height': "auto"});
4290        });
4291
4292        return this;
4293    };
4294}) (jQuery);
4295
4296/*
4297  Fullscreen Carousel
4298
4299  The following allows for an area at the top of the page that takes over the
4300  entire browser height except for its top offset and an optional bottom
4301  padding specified as a data attribute.
4302
4303  HTML:
4304
4305  <div class="fullscreen-carousel">
4306    <div class="fullscreen-carousel-content">
4307      <!-- content here -->
4308    </div>
4309    <div class="fullscreen-carousel-content">
4310      <!-- content here -->
4311    </div>
4312
4313    etc ...
4314
4315  </div>
4316
4317  Control over how the carousel takes over the screen can mostly be defined in
4318  a css file. Setting min-height on the .fullscreen-carousel-content elements
4319  will prevent them from shrinking to far vertically when the browser is very
4320  short, and setting max-height on the .fullscreen-carousel itself will prevent
4321  the area from becoming to long in the case that the browser is stretched very
4322  tall.
4323
4324  There is limited functionality for having multiple sections since that request
4325  was removed, but it is possible to add .next-arrow and .prev-arrow elements to
4326  scroll between multiple content areas.
4327*/
4328
4329(function() {
4330  $(document).ready(function() {
4331    $('.fullscreen-carousel').each(function() {
4332      initWidget(this);
4333    });
4334  });
4335
4336  function initWidget(widget) {
4337    var $widget = $(widget);
4338
4339    var topOffset = $widget.offset().top;
4340    var padBottom = parseInt($widget.data('paddingbottom')) || 0;
4341    var maxHeight = 0;
4342    var minHeight = 0;
4343    var $content = $widget.find('.fullscreen-carousel-content');
4344    var $nextArrow = $widget.find('.next-arrow');
4345    var $prevArrow = $widget.find('.prev-arrow');
4346    var $curSection = $($content[0]);
4347
4348    if ($content.length <= 1) {
4349      $nextArrow.hide();
4350      $prevArrow.hide();
4351    } else {
4352      $nextArrow.click(function() {
4353        var index = ($content.index($curSection) + 1);
4354        $curSection.hide();
4355        $curSection = $($content[index >= $content.length ? 0 : index]);
4356        $curSection.show();
4357      });
4358
4359      $prevArrow.click(function() {
4360        var index = ($content.index($curSection) - 1);
4361        $curSection.hide();
4362        $curSection = $($content[index < 0 ? $content.length - 1 : 0]);
4363        $curSection.show();
4364      });
4365    }
4366
4367    // Just hide all content sections except first.
4368    $content.each(function(index) {
4369      if ($(this).height() > minHeight) minHeight = $(this).height();
4370      $(this).css({position: 'absolute',  display: index > 0 ? 'none' : ''});
4371    });
4372
4373    // Register for changes to window size, and trigger.
4374    $(window).resize(resizeWidget);
4375    resizeWidget();
4376
4377    function resizeWidget() {
4378      var height = $(window).height() - topOffset - padBottom;
4379      $widget.width($(window).width());
4380      $widget.height(height < minHeight ? minHeight :
4381        (maxHeight && height > maxHeight ? maxHeight : height));
4382    }
4383  }
4384})();
4385
4386
4387
4388
4389
4390/*
4391  Tab Carousel
4392
4393  The following allows tab widgets to be installed via the html below. Each
4394  tab content section should have a data-tab attribute matching one of the
4395  nav items'. Also each tab content section should have a width matching the
4396  tab carousel.
4397
4398  HTML:
4399
4400  <div class="tab-carousel">
4401    <ul class="tab-nav">
4402      <li><a href="#" data-tab="handsets">Handsets</a>
4403      <li><a href="#" data-tab="wearable">Wearable</a>
4404      <li><a href="#" data-tab="tv">TV</a>
4405    </ul>
4406
4407    <div class="tab-carousel-content">
4408      <div data-tab="handsets">
4409        <!--Full width content here-->
4410      </div>
4411
4412      <div data-tab="wearable">
4413        <!--Full width content here-->
4414      </div>
4415
4416      <div data-tab="tv">
4417        <!--Full width content here-->
4418      </div>
4419    </div>
4420  </div>
4421
4422*/
4423(function() {
4424  $(document).ready(function() {
4425    $('.tab-carousel').each(function() {
4426      initWidget(this);
4427    });
4428  });
4429
4430  function initWidget(widget) {
4431    var $widget = $(widget);
4432    var $nav = $widget.find('.tab-nav');
4433    var $anchors = $nav.find('[data-tab]');
4434    var $li = $nav.find('li');
4435    var $contentContainer = $widget.find('.tab-carousel-content');
4436    var $tabs = $contentContainer.find('[data-tab]');
4437    var $curTab = $($tabs[0]); // Current tab is first tab.
4438    var width = $widget.width();
4439
4440    // Setup nav interactivity.
4441    $anchors.click(function(evt) {
4442      evt.preventDefault();
4443      var query = '[data-tab=' + $(this).data('tab') + ']';
4444      transitionWidget($tabs.filter(query));
4445    });
4446
4447    // Add highlight for navigation on first item.
4448    var $highlight = $('<div>').addClass('highlight')
4449      .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'})
4450      .appendTo($nav);
4451
4452    // Store height since we will change contents to absolute.
4453    $contentContainer.height($contentContainer.height());
4454
4455    // Absolutely position tabs so they're ready for transition.
4456    $tabs.each(function(index) {
4457      $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'});
4458    });
4459
4460    function transitionWidget($toTab) {
4461      if (!$curTab.is($toTab)) {
4462        var curIndex = $tabs.index($curTab[0]);
4463        var toIndex = $tabs.index($toTab[0]);
4464        var dir = toIndex > curIndex ? 1 : -1;
4465
4466        // Animate content sections.
4467        $toTab.css({left:(width * dir) + 'px'});
4468        $curTab.animate({left:(width * -dir) + 'px'});
4469        $toTab.animate({left:'0'});
4470
4471        // Animate navigation highlight.
4472        $highlight.animate({left:$($li[toIndex]).position().left + 'px',
4473          width:$($li[toIndex]).outerWidth() + 'px'})
4474
4475        // Store new current section.
4476        $curTab = $toTab;
4477      }
4478    }
4479  }
4480})();
4481
4482/**
4483 * Auto TOC
4484 *
4485 * Upgrades h2s on the page to have a rule and be toggle-able on mobile.
4486 */
4487(function($) {
4488  var upgraded = false;
4489  var h2Titles;
4490
4491  function initWidget() {
4492    // add HRs below all H2s (except for a few other h2 variants)
4493    // Consider doing this with css instead.
4494    h2Titles = $('h2').not('#qv h2, #tb h2, .sidebox h2, #devdoc-nav h2, h2.norule');
4495    h2Titles.css({marginBottom:0}).after('<hr/>');
4496
4497    // Exit early if on older browser.
4498    if (!window.matchMedia) {
4499      return;
4500    }
4501
4502    // Only run logic in mobile layout.
4503    var query = window.matchMedia('(max-width: 719px)');
4504    if (query.matches) {
4505      makeTogglable();
4506    } else {
4507      query.addListener(makeTogglable);
4508    }
4509  }
4510
4511  function makeTogglable() {
4512    // Only run this logic once.
4513    if (upgraded) { return; }
4514    upgraded = true;
4515
4516    // Only make content h2s togglable.
4517    var contentTitles = h2Titles.filter('#jd-content *');
4518
4519    // If there are more than 1
4520    if (contentTitles.size() < 2) {
4521      return;
4522    }
4523
4524    contentTitles.each(function() {
4525      // Find all the relevant nodes.
4526      var $title = $(this);
4527      var $hr = $title.next();
4528      var $contents = $hr.nextUntil('h2, .next-docs');
4529      var $section = $($title)
4530        .add($hr)
4531        .add($title.prev('a[name]'))
4532        .add($contents);
4533      var $anchor = $section.first().prev();
4534      var anchorMethod = 'after';
4535      if ($anchor.length === 0) {
4536        $anchor = $title.parent();
4537        anchorMethod = 'prepend';
4538      }
4539
4540      // Some h2s are in their own container making it pretty hard to find the end, so skip.
4541      if ($contents.length === 0) {
4542        return;
4543      }
4544
4545      // Remove from DOM before messing with it. DOM is slow!
4546      $section.detach();
4547
4548      // Add mobile-only expand arrows.
4549      $title.prepend('<span class="dac-visible-mobile-inline-block">' +
4550          '<i class="dac-toggle-expand dac-sprite dac-expand-more-black"></i>' +
4551          '<i class="dac-toggle-collapse dac-sprite dac-expand-less-black"></i>' +
4552          '</span>')
4553        .attr('data-toggle', 'section');
4554
4555      // Wrap in magic markup.
4556      $section = $section.wrapAll('<div class="dac-toggle dac-mobile">').parent();
4557      $contents.wrapAll('<div class="dac-toggle-content"><div>'); // extra div used for max-height calculation.
4558
4559      // Pre-expand section if requested.
4560      if ($title.hasClass('is-expanded')) {
4561        $section.addClass('is-expanded');
4562      }
4563
4564      // Pre-expand section if targetted by hash.
4565      if (location.hash && $section.find(location.hash).length) {
4566        $section.addClass('is-expanded');
4567      }
4568
4569      // Add it back to the dom.
4570      $anchor[anchorMethod].call($anchor, $section);
4571    });
4572  }
4573
4574  $(function() {
4575    initWidget();
4576  });
4577})(jQuery);
4578
4579(function($) {
4580  'use strict';
4581
4582  /**
4583   * Toggle Floating Label state.
4584   * @param {HTMLElement} el - The DOM element.
4585   * @param options
4586   * @constructor
4587   */
4588  function FloatingLabel(el, options) {
4589    this.el = $(el);
4590    this.options = $.extend({}, FloatingLabel.DEFAULTS_, options);
4591    this.group = this.el.closest('.dac-form-input-group');
4592    this.input = this.group.find('.dac-form-input');
4593
4594    this.checkValue_ = this.checkValue_.bind(this);
4595    this.checkValue_();
4596
4597    this.input.on('focus', function() {
4598      this.group.addClass('dac-focused');
4599    }.bind(this));
4600    this.input.on('blur', function() {
4601      this.group.removeClass('dac-focused');
4602      this.checkValue_();
4603    }.bind(this));
4604    this.input.on('keyup', this.checkValue_);
4605  }
4606
4607  /**
4608   * The label is moved out of the textbox when it has a value.
4609   */
4610  FloatingLabel.prototype.checkValue_ = function() {
4611    if (this.input.val().length) {
4612      this.group.addClass('dac-has-value');
4613    } else {
4614      this.group.removeClass('dac-has-value');
4615    }
4616  };
4617
4618  /**
4619   * jQuery plugin
4620   * @param  {object} options - Override default options.
4621   */
4622  $.fn.dacFloatingLabel = function(options) {
4623    return this.each(function() {
4624      new FloatingLabel(this, options);
4625    });
4626  };
4627
4628  $(document).on('ready.aranja', function() {
4629    $('.dac-form-floatlabel').each(function() {
4630      $(this).dacFloatingLabel($(this).data());
4631    });
4632  });
4633})(jQuery);
4634
4635/* global toRoot, CAROUSEL_OVERRIDE */
4636(function($) {
4637  // Ordering matters
4638  var TAG_MAP = [
4639    {from: 'developerstory', to: 'Android Developer Story'},
4640    {from: 'googleplay', to: 'Google Play'}
4641  ];
4642
4643  function DacCarouselQuery(el) {
4644    this.el = $(el);
4645
4646    var opts = this.el.data();
4647    opts.maxResults = parseInt(opts.maxResults || '100', 10);
4648    opts.query = opts.carouselQuery;
4649    var resources = $.queryResources(opts);
4650
4651    this.el.empty();
4652    $(resources).map(function() {
4653      var resource = $.extend({}, this, CAROUSEL_OVERRIDE[this.url]);
4654      var slide = $('<article class="dac-expand dac-hero">');
4655      var image = cleanUrl(resource.heroImage || resource.image);
4656      var fullBleed = image && !resource.heroColor;
4657
4658      // Configure background
4659      slide.css({
4660        backgroundImage: fullBleed ? 'url(' + image + ')' : '',
4661        backgroundColor: resource.heroColor || ''
4662      });
4663
4664      // Should copy be inverted
4665      slide.toggleClass('dac-invert', resource.heroInvert || fullBleed);
4666      slide.toggleClass('dac-darken', fullBleed);
4667
4668      // Should be clickable
4669      slide.append($('<a class="dac-hero-carousel-action">').attr('href', cleanUrl(resource.url)));
4670
4671      var cols = $('<div class="cols dac-hero-content">');
4672
4673      // inline image column
4674      var rightCol = $('<div class="col-1of2 col-push-1of2 dac-hero-figure">')
4675        .appendTo(cols);
4676
4677      if (!fullBleed && image) {
4678        rightCol.append($('<img>').attr('src', image));
4679      }
4680
4681      // info column
4682      $('<div class="col-1of2 col-pull-1of2">')
4683        .append($('<div class="dac-hero-tag">').text(formatTag(resource)))
4684        .append($('<h1 class="dac-hero-title">').text(formatTitle(resource)))
4685        .append($('<p class="dac-hero-description">').text(resource.summary))
4686        .append($('<a class="dac-hero-cta">')
4687          .text(formatCTA(resource))
4688          .attr('href', cleanUrl(resource.url))
4689          .prepend($('<span class="dac-sprite dac-auto-chevron">'))
4690        )
4691        .appendTo(cols);
4692
4693      slide.append(cols.wrap('<div class="wrap">').parent());
4694      return slide[0];
4695    }).prependTo(this.el);
4696
4697    // Pagination element.
4698    this.el.append('<div class="dac-hero-carousel-pagination"><div class="wrap" data-carousel-pagination>');
4699
4700    this.el.dacCarousel();
4701  }
4702
4703  function cleanUrl(url) {
4704    if (url && url.indexOf('//') === -1) {
4705      url = toRoot + url;
4706    }
4707    return url;
4708  }
4709
4710  function formatTag(resource) {
4711    // Hmm, need a better more scalable solution for this.
4712    for (var i = 0, mapping; mapping = TAG_MAP[i]; i++) {
4713      if (resource.tags.indexOf(mapping.from) > -1) {
4714        return mapping.to;
4715      }
4716    }
4717    return resource.type;
4718  }
4719
4720  function formatTitle(resource) {
4721    return resource.title.replace(/android developer story: /i, '');
4722  }
4723
4724  function formatCTA(resource) {
4725    return resource.type === 'youtube' ? 'Watch the video' : 'Learn more';
4726  }
4727
4728  // jQuery plugin
4729  $.fn.dacCarouselQuery = function() {
4730    return this.each(function() {
4731      var el = $(this);
4732      var data = el.data('dac.carouselQuery');
4733
4734      if (!data) { el.data('dac.carouselQuery', (data = new DacCarouselQuery(el))); }
4735    });
4736  };
4737
4738  // Data API
4739  $(function() {
4740    $('[data-carousel-query]').dacCarouselQuery();
4741  });
4742})(jQuery);
4743
4744(function($) {
4745  /**
4746   * A CSS based carousel, inspired by SequenceJS.
4747   * @param {jQuery} el
4748   * @param {object} options
4749   * @constructor
4750   */
4751  function DacCarousel(el, options) {
4752    this.el = $(el);
4753    this.options = options = $.extend({}, DacCarousel.OPTIONS, this.el.data(), options || {});
4754    this.frames = this.el.find(options.frameSelector);
4755    this.count = this.frames.size();
4756    this.current = options.start;
4757
4758    this.initPagination();
4759    this.initEvents();
4760    this.initFrame();
4761  }
4762
4763  DacCarousel.OPTIONS = {
4764    auto:      true,
4765    autoTime:  10000,
4766    autoMinTime: 5000,
4767    btnPrev:   '[data-carousel-prev]',
4768    btnNext:   '[data-carousel-next]',
4769    frameSelector: 'article',
4770    loop:      true,
4771    start:     0,
4772    swipeThreshold: 160,
4773    pagination: '[data-carousel-pagination]'
4774  };
4775
4776  DacCarousel.prototype.initPagination = function() {
4777    this.pagination = $([]);
4778    if (!this.options.pagination) { return; }
4779
4780    var pagination = $('<ul class="dac-pagination">');
4781    var parent = this.el;
4782    if (typeof this.options.pagination === 'string') { parent = this.el.find(this.options.pagination); }
4783
4784    if (this.count > 1) {
4785      for (var i = 0; i < this.count; i++) {
4786        var li = $('<li class="dac-pagination-item">').text(i);
4787        if (i === this.options.start) { li.addClass('active'); }
4788        li.click(this.go.bind(this, i));
4789
4790        pagination.append(li);
4791      }
4792      this.pagination = pagination.children();
4793      parent.append(pagination);
4794    }
4795  };
4796
4797  DacCarousel.prototype.initEvents = function() {
4798    var that = this;
4799
4800    this.touch = {
4801      start: {x: 0, y: 0},
4802      end:   {x: 0, y: 0}
4803    };
4804
4805    this.el.on('touchstart', this.touchstart_.bind(this));
4806    this.el.on('touchend', this.touchend_.bind(this));
4807    this.el.on('touchmove', this.touchmove_.bind(this));
4808
4809    this.el.hover(function() {
4810      that.pauseRotateTimer();
4811    }, function() {
4812      that.startRotateTimer();
4813    });
4814
4815    $(this.options.btnPrev).click(function(e) {
4816      e.preventDefault();
4817      that.prev();
4818    });
4819
4820    $(this.options.btnNext).click(function(e) {
4821      e.preventDefault();
4822      that.next();
4823    });
4824  };
4825
4826  DacCarousel.prototype.touchstart_ = function(event) {
4827    var t = event.originalEvent.touches[0];
4828    this.touch.start = {x: t.screenX, y: t.screenY};
4829  };
4830
4831  DacCarousel.prototype.touchend_ = function() {
4832    var deltaX = this.touch.end.x - this.touch.start.x;
4833    var deltaY = Math.abs(this.touch.end.y - this.touch.start.y);
4834    var shouldSwipe = (deltaY < Math.abs(deltaX)) && (Math.abs(deltaX) >= this.options.swipeThreshold);
4835
4836    if (shouldSwipe) {
4837      if (deltaX > 0) {
4838        this.prev();
4839      } else {
4840        this.next();
4841      }
4842    }
4843  };
4844
4845  DacCarousel.prototype.touchmove_ = function(event) {
4846    var t = event.originalEvent.touches[0];
4847    this.touch.end = {x: t.screenX, y: t.screenY};
4848  };
4849
4850  DacCarousel.prototype.initFrame = function() {
4851    this.frames.removeClass('active').eq(this.options.start).addClass('active');
4852  };
4853
4854  DacCarousel.prototype.startRotateTimer = function() {
4855    if (!this.options.auto || this.rotateTimer) { return; }
4856    this.rotateTimer = setTimeout(this.next.bind(this), this.options.autoTime);
4857  };
4858
4859  DacCarousel.prototype.pauseRotateTimer = function() {
4860    clearTimeout(this.rotateTimer);
4861    this.rotateTimer = null;
4862  };
4863
4864  DacCarousel.prototype.prev = function() {
4865    this.go(this.current - 1);
4866  };
4867
4868  DacCarousel.prototype.next = function() {
4869    this.go(this.current + 1);
4870  };
4871
4872  DacCarousel.prototype.go = function(next) {
4873    // Figure out what the next slide is.
4874    while (this.count > 0 && next >= this.count) { next -= this.count; }
4875    while (next < 0) { next += this.count; }
4876
4877    // Cancel if we're already on that slide.
4878    if (next === this.current) { return; }
4879
4880    // Prepare next slide.
4881    this.frames.eq(next).removeClass('out');
4882
4883    // Recalculate styles before starting slide transition.
4884    this.el.resolveStyles();
4885    // Update pagination
4886    this.pagination.removeClass('active').eq(next).addClass('active');
4887
4888    // Transition out current frame
4889    this.frames.eq(this.current).toggleClass('active out');
4890
4891    // Transition in a new frame
4892    this.frames.eq(next).toggleClass('active');
4893
4894    this.current = next;
4895  };
4896
4897  // Helper which resolves new styles for an element, so it can start transitioning
4898  // from the new values.
4899  $.fn.resolveStyles = function() {
4900    /*jshint expr:true*/
4901    this[0] && this[0].offsetTop;
4902    return this;
4903  };
4904
4905  // jQuery plugin
4906  $.fn.dacCarousel = function() {
4907    this.each(function() {
4908      var $el = $(this);
4909      $el.data('dac-carousel', new DacCarousel(this));
4910    });
4911    return this;
4912  };
4913
4914  // Data API
4915  $(function() {
4916    $('[data-carousel]').dacCarousel();
4917  });
4918})(jQuery);
4919
4920(function($) {
4921  'use strict';
4922
4923  function Modal(el, options) {
4924    this.el = $(el);
4925    this.options = $.extend({}, ToggleModal.DEFAULTS_, options);
4926    this.isOpen = false;
4927
4928    this.el.on('click', function(event) {
4929      if (!$.contains($('.dac-modal-window')[0], event.target)) {
4930        return this.el.trigger('modal-close');
4931      }
4932    }.bind(this));
4933
4934    this.el.on('modal-open', this.open_.bind(this));
4935    this.el.on('modal-close', this.close_.bind(this));
4936    this.el.on('modal-toggle', this.toggle_.bind(this));
4937  }
4938
4939  Modal.prototype.toggle_ = function() {
4940    this.el.trigger('modal-' + (this.isOpen ? 'close' : 'open'));
4941  };
4942
4943  Modal.prototype.close_ = function() {
4944    this.el.removeClass('dac-active');
4945    $('body').removeClass('dac-modal-open');
4946    this.isOpen = false;
4947  };
4948
4949  Modal.prototype.open_ = function() {
4950    this.el.addClass('dac-active');
4951    $('body').addClass('dac-modal-open');
4952    this.isOpen = true;
4953  };
4954
4955  function ToggleModal(el, options) {
4956    this.el = $(el);
4957    this.options = $.extend({}, ToggleModal.DEFAULTS_, options);
4958    this.modal = this.options.modalToggle ? $('[data-modal="' + this.options.modalToggle + '"]') :
4959      this.el.closest('[data-modal]');
4960
4961    this.el.on('click', this.clickHandler_.bind(this));
4962  }
4963
4964  ToggleModal.prototype.clickHandler_ = function(event) {
4965    event.preventDefault();
4966    this.modal.trigger('modal-toggle');
4967  };
4968
4969  /**
4970   * jQuery plugin
4971   * @param  {object} options - Override default options.
4972   */
4973  $.fn.dacModal = function(options) {
4974    return this.each(function() {
4975      new Modal(this, options);
4976    });
4977  };
4978
4979  $.fn.dacToggleModal = function(options) {
4980    return this.each(function() {
4981      new ToggleModal(this, options);
4982    });
4983  };
4984
4985  /**
4986   * Data Attribute API
4987   */
4988  $(document).on('ready.aranja', function() {
4989    $('[data-modal]').each(function() {
4990      $(this).dacModal($(this).data());
4991    });
4992
4993    $('[data-modal-toggle]').each(function() {
4994      $(this).dacToggleModal($(this).data());
4995    });
4996  });
4997})(jQuery);
4998
4999(function($) {
5000  'use strict';
5001
5002  /**
5003   * Toggle the visabilty of the mobile navigation.
5004   * @param {HTMLElement} el - The DOM element.
5005   * @param options
5006   * @constructor
5007   */
5008  function ToggleNav(el, options) {
5009    this.el = $(el);
5010    this.options = $.extend({}, ToggleNav.DEFAULTS_, options);
5011    this.options.target = [this.options.navigation];
5012
5013    if (this.options.body) {this.options.target.push('body')}
5014    if (this.options.dimmer) {this.options.target.push(this.options.dimmer)}
5015
5016    this.el.on('click', this.clickHandler_.bind(this));
5017  }
5018
5019  /**
5020   * ToggleNav Default Settings
5021   * @type {{body: boolean, dimmer: string, navigation: string, toggleClass: string}}
5022   * @private
5023   */
5024  ToggleNav.DEFAULTS_ = {
5025    body: true,
5026    dimmer: '.dac-nav-dimmer',
5027    navigation: '[data-dac-nav]',
5028    toggleClass: 'dac-nav-open'
5029  };
5030
5031  /**
5032   * The actual toggle logic.
5033   * @param event
5034   * @private
5035   */
5036  ToggleNav.prototype.clickHandler_ = function(event) {
5037    event.preventDefault();
5038    $(this.options.target.join(', ')).toggleClass(this.options.toggleClass);
5039  };
5040
5041  /**
5042   * jQuery plugin
5043   * @param  {object} options - Override default options.
5044   */
5045  $.fn.dacToggleMobileNav = function(options) {
5046    return this.each(function() {
5047      new ToggleNav(this, options);
5048    });
5049  };
5050
5051  /**
5052   * Data Attribute API
5053   */
5054  $(window).on('load.aranja', function() {
5055    $('[data-dac-toggle-nav]').each(function() {
5056      $(this).dacToggleMobileNav($(this).data());
5057    });
5058  });
5059})(jQuery);
5060
5061(function($) {
5062  'use strict';
5063
5064  /**
5065   * Submit the newsletter form to a Google Form.
5066   * @param {HTMLElement} el - The Form DOM element.
5067   * @constructor
5068   */
5069  function NewsletterForm(el) {
5070    this.el = $(el);
5071    this.form = this.el.find('form');
5072    $('<iframe/>').hide()
5073      .attr('name', 'dac-newsletter-iframe')
5074      .attr('src', '')
5075      .insertBefore(this.form);
5076    this.form.on('submit', this.submitHandler_.bind(this));
5077  }
5078
5079  /**
5080   * Milliseconds until modal has vanished after modal-close is triggered.
5081   * @type {number}
5082   * @private
5083   */
5084  NewsletterForm.CLOSE_DELAY_ = 300;
5085
5086  /**
5087   * Switch view to display form after close.
5088   * @private
5089   */
5090  NewsletterForm.prototype.closeHandler_ = function() {
5091    setTimeout(function() {
5092      this.el.trigger('swap-reset');
5093    }.bind(this), NewsletterForm.CLOSE_DELAY_);
5094  };
5095
5096  /**
5097   * Reset the modal to initial state.
5098   * @private
5099   */
5100  NewsletterForm.prototype.reset_ = function() {
5101    this.form.trigger('reset');
5102    this.el.one('modal-close', this.closeHandler_.bind(this));
5103  };
5104
5105  /**
5106   * Display a success view on submit.
5107   * @private
5108   */
5109  NewsletterForm.prototype.submitHandler_ = function() {
5110    this.el.one('swap-complete', this.reset_.bind(this));
5111    this.el.trigger('swap-content');
5112  };
5113
5114  /**
5115   * jQuery plugin
5116   * @param  {object} options - Override default options.
5117   */
5118  $.fn.dacNewsletterForm = function(options) {
5119    return this.each(function() {
5120      new NewsletterForm(this, options);
5121    });
5122  };
5123
5124  /**
5125   * Data Attribute API
5126   */
5127  $(document).on('ready.aranja', function() {
5128    $('[data-newsletter]').each(function() {
5129      $(this).dacNewsletterForm();
5130    });
5131  });
5132})(jQuery);
5133
5134(function($) {
5135  'use strict';
5136
5137  /**
5138   * Smoothly scroll to location on current page.
5139   * @param el
5140   * @param options
5141   * @constructor
5142   */
5143  function ScrollButton(el, options) {
5144    this.el = $(el);
5145    this.target = $(this.el.attr('href'));
5146    this.options = $.extend({}, ScrollButton.DEFAULTS_, options);
5147
5148    if (typeof this.options.offset === 'string') {
5149      this.options.offset = $(this.options.offset).height();
5150    }
5151
5152    this.el.on('click', this.clickHandler_.bind(this));
5153  }
5154
5155  /**
5156   * Default options
5157   * @type {{duration: number, easing: string, offset: number, scrollContainer: string}}
5158   * @private
5159   */
5160  ScrollButton.DEFAULTS_ = {
5161    duration: 300,
5162    easing: 'swing',
5163    offset: 0,
5164    scrollContainer: 'html, body'
5165  };
5166
5167  /**
5168   * Scroll logic
5169   * @param event
5170   * @private
5171   */
5172  ScrollButton.prototype.clickHandler_ = function(event) {
5173    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
5174      return;
5175    }
5176
5177    event.preventDefault();
5178
5179    $(this.options.scrollContainer).animate({
5180      scrollTop: this.target.offset().top - this.options.offset
5181    }, this.options);
5182  };
5183
5184  /**
5185   * jQuery plugin
5186   * @param  {object} options - Override default options.
5187   */
5188  $.fn.dacScrollButton = function(options) {
5189    return this.each(function() {
5190      new ScrollButton(this, options);
5191    });
5192  };
5193
5194  /**
5195   * Data Attribute API
5196   */
5197  $(document).on('ready.aranja', function() {
5198    $('[data-scroll-button]').each(function() {
5199      $(this).dacScrollButton($(this).data());
5200    });
5201  });
5202})(jQuery);
5203
5204(function($) {
5205  'use strict';
5206
5207  /**
5208   * A component that swaps two dynamic height views with an animation.
5209   * Listens for the following events:
5210   * * swap-content: triggers SwapContent.swap_()
5211   * * swap-reset: triggers SwapContent.reset()
5212   * @param el
5213   * @param options
5214   * @constructor
5215   */
5216  function SwapContent(el, options) {
5217    this.el = $(el);
5218    this.options = $.extend({}, SwapContent.DEFAULTS_, options);
5219    this.containers = this.el.find(this.options.container);
5220    this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0);
5221    this.el.on('swap-content', this.swap.bind(this));
5222    this.el.on('swap-reset', this.reset.bind(this));
5223  }
5224
5225  /**
5226   * SwapContent's default settings.
5227   * @type {{activeClass: string, container: string, transitionSpeed: number}}
5228   * @private
5229   */
5230  SwapContent.DEFAULTS_ = {
5231    activeClass: 'dac-active',
5232    container: '[data-swap-container]',
5233    transitionSpeed: 500
5234  };
5235
5236  /**
5237   * Returns container's visible height.
5238   * @param container
5239   * @returns {number}
5240   */
5241  SwapContent.prototype.currentHeight = function(container) {
5242    return container.children('.' + this.options.activeClass).outerHeight();
5243  };
5244
5245  /**
5246   * Reset to show initial content
5247   */
5248  SwapContent.prototype.reset = function() {
5249    if (!this.initiallyActive.hasClass(this.initiallyActive)) {
5250      this.containers.children().toggleClass(this.options.activeClass);
5251    }
5252  };
5253
5254  /**
5255   * Complete the swap.
5256   */
5257  SwapContent.prototype.complete = function() {
5258    this.containers.height('auto');
5259    this.containers.trigger('swap-complete');
5260  };
5261
5262  /**
5263   * Perform the swap of content.
5264   */
5265  SwapContent.prototype.swap = function() {
5266    console.log(this.containers);
5267    this.containers.each(function(index, container) {
5268      container = $(container);
5269      container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass);
5270      container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed,
5271        this.complete.bind(this));
5272    }.bind(this));
5273  };
5274
5275  /**
5276   * jQuery plugin
5277   * @param  {object} options - Override default options.
5278   */
5279  $.fn.dacSwapContent = function(options) {
5280    return this.each(function() {
5281      new SwapContent(this, options);
5282    });
5283  };
5284
5285  /**
5286   * Data Attribute API
5287   */
5288  $(document).on('ready.aranja', function() {
5289    $('[data-swap]').each(function() {
5290      $(this).dacSwapContent($(this).data());
5291    });
5292  });
5293})(jQuery);
5294
5295(function($) {
5296  function Toggle(el) {
5297    $(el).on('click.dac.togglesection', this.toggle);
5298  }
5299
5300  Toggle.prototype.toggle = function() {
5301    var $this = $(this);
5302
5303    var $parent = getParent($this);
5304    var isExpanded = $parent.hasClass('is-expanded');
5305
5306    transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded);
5307    $parent.toggleClass('is-expanded');
5308
5309    return false;
5310  };
5311
5312  function getParent($this) {
5313    var selector = $this.attr('data-target');
5314
5315    if (!selector) {
5316      selector = $this.attr('href');
5317      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
5318    }
5319
5320    var $parent = selector && $(selector);
5321
5322    $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle');
5323
5324    return $parent.length ? $parent : $this.parent();
5325  }
5326
5327  /**
5328   * Runs a transition of max-height along with responsive styles which hide or expand the element.
5329   * @param $el
5330   * @param visible
5331   */
5332  function transitionMaxHeight($el, visible) {
5333    var contentHeight = $el.prop('scrollHeight');
5334    var targetHeight = visible ? contentHeight : 0;
5335    var duration = $el.transitionDuration();
5336
5337    // If we're hiding, first set the maxHeight we're transitioning from.
5338    if (!visible) {
5339      $el.css('maxHeight', contentHeight + 'px')
5340        .resolveStyles();
5341    }
5342
5343    // Transition to new state
5344    $el.css('maxHeight', targetHeight);
5345
5346    // Reset maxHeight to css value after transition.
5347    setTimeout(function() {
5348      $el.css('maxHeight', '');
5349    }, duration);
5350  }
5351
5352  // Utility to get the transition duration for the element.
5353  $.fn.transitionDuration = function() {
5354    var d = $(this).css('transitionDuration') || '0s';
5355
5356    return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0);
5357  };
5358
5359  // jQuery plugin
5360  $.fn.toggleSection = function(option) {
5361    return this.each(function() {
5362      var $this = $(this);
5363      var data = $this.data('dac.togglesection');
5364      if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));}
5365      if (typeof option === 'string') {data[option].call($this);}
5366    });
5367  };
5368
5369  // Data api
5370  $(document)
5371    .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle);
5372})(jQuery);
5373