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