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