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