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