var classesNav; var devdocNav; var sidenav; var cookie_namespace = 'android_developer'; var NAV_PREF_TREE = "tree"; var NAV_PREF_PANELS = "panels"; var nav_pref; var isMobile = false; // true if mobile, so we can adjust some layout var mPagePath; // initialized in ready() function var basePath = getBaseUri(location.pathname); var SITE_ROOT = toRoot + basePath.substring(1,basePath.indexOf("/",1)); var GOOGLE_DATA; // combined data for google service apis, used for search suggest // Ensure that all ajax getScript() requests allow caching $.ajaxSetup({ cache: true }); /****** ON LOAD SET UP STUFF *********/ $(document).ready(function() { // show lang dialog if the URL includes /intl/ //if (location.pathname.substring(0,6) == "/intl/") { // var lang = location.pathname.split('/')[2]; // if (lang != getLangPref()) { // $("#langMessage a.yes").attr("onclick","changeLangPref('" + lang // + "', true); $('#langMessage').hide(); return false;"); // $("#langMessage .lang." + lang).show(); // $("#langMessage").show(); // } //} // load json file for JD doc search suggestions $.getScript(toRoot + 'jd_lists_unified.js'); // load json file for Android API search suggestions $.getScript(toRoot + 'reference/lists.js'); // load json files for Google services API suggestions $.getScript(toRoot + 'reference/gcm_lists.js', function(data, textStatus, jqxhr) { // once the GCM json (GCM_DATA) is loaded, load the GMS json (GMS_DATA) and merge the data if(jqxhr.status === 200) { $.getScript(toRoot + 'reference/gms_lists.js', function(data, textStatus, jqxhr) { if(jqxhr.status === 200) { // combine GCM and GMS data GOOGLE_DATA = GMS_DATA; var start = GOOGLE_DATA.length; for (var i=0; i<GCM_DATA.length; i++) { GOOGLE_DATA.push({id:start+i, label:GCM_DATA[i].label, link:GCM_DATA[i].link, type:GCM_DATA[i].type}); } } }); } }); // setup keyboard listener for search shortcut $('body').keyup(function(event) { if (event.which == 191) { $('#search_autocomplete').focus(); } }); // init the fullscreen toggle click event $('#nav-swap .fullscreen').click(function(){ if ($(this).hasClass('disabled')) { toggleFullscreen(true); } else { toggleFullscreen(false); } }); // initialize the divs with custom scrollbars $('.scroll-pane').jScrollPane( {verticalGutter:0} ); // add HRs below all H2s (except for a few other h2 variants) $('h2').not('#qv h2') .not('#tb h2') .not('.sidebox h2') .not('#devdoc-nav h2') .not('h2.norule').css({marginBottom:0}) .after('<hr/>'); // set up the search close button $('.search .close').click(function() { $searchInput = $('#search_autocomplete'); $searchInput.attr('value', ''); $(this).addClass("hide"); $("#search-container").removeClass('active'); $("#search_autocomplete").blur(); search_focus_changed($searchInput.get(), false); hideResults(); }); // Set up quicknav var quicknav_open = false; $("#btn-quicknav").click(function() { if (quicknav_open) { $(this).removeClass('active'); quicknav_open = false; collapse(); } else { $(this).addClass('active'); quicknav_open = true; expand(); } }) var expand = function() { $('#header-wrap').addClass('quicknav'); $('#quicknav').stop().show().animate({opacity:'1'}); } var collapse = function() { $('#quicknav').stop().animate({opacity:'0'}, 100, function() { $(this).hide(); $('#header-wrap').removeClass('quicknav'); }); } //Set up search $("#search_autocomplete").focus(function() { $("#search-container").addClass('active'); }) $("#search-container").mouseover(function() { $("#search-container").addClass('active'); $("#search_autocomplete").focus(); }) $("#search-container").mouseout(function() { if ($("#search_autocomplete").is(":focus")) return; if ($("#search_autocomplete").val() == '') { setTimeout(function(){ $("#search-container").removeClass('active'); $("#search_autocomplete").blur(); },250); } }) $("#search_autocomplete").blur(function() { if ($("#search_autocomplete").val() == '') { $("#search-container").removeClass('active'); } }) // prep nav expandos var pagePath = document.location.pathname; // account for intl docs by removing the intl/*/ path if (pagePath.indexOf("/intl/") == 0) { pagePath = pagePath.substr(pagePath.indexOf("/",6)); // start after intl/ to get last / } if (pagePath.indexOf(SITE_ROOT) == 0) { if (pagePath == '' || pagePath.charAt(pagePath.length - 1) == '/') { pagePath += 'index.html'; } } // Need a copy of the pagePath before it gets changed in the next block; // it's needed to perform proper tab highlighting in offline docs (see rootDir below) var pagePathOriginal = pagePath; if (SITE_ROOT.match(/\.\.\//) || SITE_ROOT == '') { // If running locally, SITE_ROOT will be a relative path, so account for that by // finding the relative URL to this page. This will allow us to find links on the page // leading back to this page. var pathParts = pagePath.split('/'); var relativePagePathParts = []; var upDirs = (SITE_ROOT.match(/(\.\.\/)+/) || [''])[0].length / 3; for (var i = 0; i < upDirs; i++) { relativePagePathParts.push('..'); } for (var i = 0; i < upDirs; i++) { relativePagePathParts.push(pathParts[pathParts.length - (upDirs - i) - 1]); } relativePagePathParts.push(pathParts[pathParts.length - 1]); pagePath = relativePagePathParts.join('/'); } else { // Otherwise the page path is already an absolute URL } // Highlight the header tabs... // highlight Design tab if ($("body").hasClass("design")) { $("#header li.design a").addClass("selected"); $("#sticky-header").addClass("design"); // highlight About tabs } else if ($("body").hasClass("about")) { var rootDir = pagePathOriginal.substring(1,pagePathOriginal.indexOf('/', 1)); if (rootDir == "about") { $("#nav-x li.about a").addClass("selected"); } else if (rootDir == "wear") { $("#nav-x li.wear a").addClass("selected"); } else if (rootDir == "tv") { $("#nav-x li.tv a").addClass("selected"); } else if (rootDir == "auto") { $("#nav-x li.auto a").addClass("selected"); } // highlight Develop tab } else if ($("body").hasClass("develop") || $("body").hasClass("google")) { $("#header li.develop a").addClass("selected"); $("#sticky-header").addClass("develop"); // In Develop docs, also highlight appropriate sub-tab var rootDir = pagePathOriginal.substring(1,pagePathOriginal.indexOf('/', 1)); if (rootDir == "training") { $("#nav-x li.training a").addClass("selected"); } else if (rootDir == "guide") { $("#nav-x li.guide a").addClass("selected"); } else if (rootDir == "reference") { // If the root is reference, but page is also part of Google Services, select Google if ($("body").hasClass("google")) { $("#nav-x li.google a").addClass("selected"); } else { $("#nav-x li.reference a").addClass("selected"); } } else if ((rootDir == "tools") || (rootDir == "sdk")) { $("#nav-x li.tools a").addClass("selected"); } else if ($("body").hasClass("google")) { $("#nav-x li.google a").addClass("selected"); } else if ($("body").hasClass("samples")) { $("#nav-x li.samples a").addClass("selected"); } else if (rootDir == "ndk") { if ($("body").hasClass("guide")) { $("#nav-x li.guide a").addClass("selected"); } else if ($("body").hasClass("samples")) { $("#nav-x li.samples a").addClass("selected"); } else if ($("body").hasClass("downloads")) { $("#nav-x li.downloads a").addClass("selected"); } else if ($("body").hasClass("reference")) { $("#nav-x li.reference a").addClass("selected"); } } // highlight Distribute tab } else if ($("body").hasClass("distribute")) { $("#header li.distribute a").addClass("selected"); $("#sticky-header").addClass("distribute"); var baseFrag = pagePathOriginal.indexOf('/', 1) + 1; var secondFrag = pagePathOriginal.substring(baseFrag, pagePathOriginal.indexOf('/', baseFrag)); if (secondFrag == "users") { $("#nav-x li.users a").addClass("selected"); } else if (secondFrag == "engage") { $("#nav-x li.engage a").addClass("selected"); } else if (secondFrag == "monetize") { $("#nav-x li.monetize a").addClass("selected"); } else if (secondFrag == "analyze") { $("#nav-x li.analyze a").addClass("selected"); } else if (secondFrag == "tools") { $("#nav-x li.disttools a").addClass("selected"); } else if (secondFrag == "stories") { $("#nav-x li.stories a").addClass("selected"); } else if (secondFrag == "essentials") { $("#nav-x li.essentials a").addClass("selected"); } else if (secondFrag == "googleplay") { $("#nav-x li.googleplay a").addClass("selected"); } } else if ($("body").hasClass("about")) { $("#sticky-header").addClass("about"); } // set global variable so we can highlight the sidenav a bit later (such as for google reference) // and highlight the sidenav mPagePath = pagePath; highlightSidenav(); buildBreadcrumbs(); // set up prev/next links if they exist var $selNavLink = $('#nav').find('a[href="' + pagePath + '"]'); var $selListItem; if ($selNavLink.length) { $selListItem = $selNavLink.closest('li'); // set up prev links var $prevLink = []; var $prevListItem = $selListItem.prev('li'); var crossBoundaries = ($("body.design").length > 0) || ($("body.guide").length > 0) ? true : false; // navigate across topic boundaries only in design docs if ($prevListItem.length) { if ($prevListItem.hasClass('nav-section') || crossBoundaries) { // jump to last topic of previous section $prevLink = $prevListItem.find('a:last'); } else if (!$selListItem.hasClass('nav-section')) { // jump to previous topic in this section $prevLink = $prevListItem.find('a:eq(0)'); } } else { // jump to this section's index page (if it exists) var $parentListItem = $selListItem.parents('li'); $prevLink = $selListItem.parents('li').find('a'); // except if cross boundaries aren't allowed, and we're at the top of a section already // (and there's another parent) if (!crossBoundaries && $parentListItem.hasClass('nav-section') && $selListItem.hasClass('nav-section')) { $prevLink = []; } } // set up next links var $nextLink = []; var startClass = false; var isCrossingBoundary = false; if ($selListItem.hasClass('nav-section') && $selListItem.children('div.empty').length == 0) { // we're on an index page, jump to the first topic $nextLink = $selListItem.find('ul:eq(0)').find('a:eq(0)'); // if there aren't any children, go to the next section (required for About pages) if($nextLink.length == 0) { $nextLink = $selListItem.next('li').find('a'); } else if ($('.topic-start-link').length) { // as long as there's a child link and there is a "topic start link" (we're on a landing) // then set the landing page "start link" text to be the first doc title $('.topic-start-link').text($nextLink.text().toUpperCase()); } // If the selected page has a description, then it's a class or article homepage if ($selListItem.find('a[description]').length) { // this means we're on a class landing page startClass = true; } } else { // jump to the next topic in this section (if it exists) $nextLink = $selListItem.next('li').find('a:eq(0)'); if ($nextLink.length == 0) { isCrossingBoundary = true; // no more topics in this section, jump to the first topic in the next section $nextLink = $selListItem.parents('li:eq(0)').next('li').find('a:eq(0)'); if (!$nextLink.length) { // Go up another layer to look for next page (lesson > class > course) $nextLink = $selListItem.parents('li:eq(1)').next('li.nav-section').find('a:eq(0)'); if ($nextLink.length == 0) { // if that doesn't work, we're at the end of the list, so disable NEXT link $('.next-page-link').attr('href','').addClass("disabled") .click(function() { return false; }); // and completely hide the one in the footer $('.content-footer .next-page-link').hide(); } } } } if (startClass) { $('.start-class-link').attr('href', $nextLink.attr('href')).removeClass("hide"); // if there's no training bar (below the start button), // then we need to add a bottom border to button if (!$("#tb").length) { $('.start-class-link').css({'border-bottom':'1px solid #DADADA'}); } } else if (isCrossingBoundary && !$('body.design').length) { // Design always crosses boundaries $('.content-footer.next-class').show(); $('.next-page-link').attr('href','') .removeClass("hide").addClass("disabled") .click(function() { return false; }); // and completely hide the one in the footer $('.content-footer .next-page-link').hide(); if ($nextLink.length) { $('.next-class-link').attr('href',$nextLink.attr('href')) .removeClass("hide") .append(": " + $nextLink.html()); $('.next-class-link').find('.new').empty(); } } else { $('.next-page-link').attr('href', $nextLink.attr('href')) .removeClass("hide"); // for the footer link, also add the next page title $('.content-footer .next-page-link').append(": " + $nextLink.html()); } if (!startClass && $prevLink.length) { var prevHref = $prevLink.attr('href'); if (prevHref == SITE_ROOT + 'index.html') { // Don't show Previous when it leads to the homepage } else { $('.prev-page-link').attr('href', $prevLink.attr('href')).removeClass("hide"); } } } // Set up the course landing pages for Training with class names and descriptions if ($('body.trainingcourse').length) { var $classLinks = $selListItem.find('ul li a').not('#nav .nav-section .nav-section ul a'); // create an array for all the class descriptions var $classDescriptions = new Array($classLinks.length); var lang = getLangPref(); $classLinks.each(function(index) { var langDescr = $(this).attr(lang + "-description"); if (typeof langDescr !== 'undefined' && langDescr !== false) { // if there's a class description in the selected language, use that $classDescriptions[index] = langDescr; } else { // otherwise, use the default english description $classDescriptions[index] = $(this).attr("description"); } }); var $olClasses = $('<ol class="class-list"></ol>'); var $liClass; var $imgIcon; var $h2Title; var $pSummary; var $olLessons; var $liLesson; $classLinks.each(function(index) { $liClass = $('<li></li>'); $h2Title = $('<a class="title" href="'+$(this).attr('href')+'"><h2>' + $(this).html()+'</h2><span></span></a>'); $pSummary = $('<p class="description">' + $classDescriptions[index] + '</p>'); $olLessons = $('<ol class="lesson-list"></ol>'); $lessons = $(this).closest('li').find('ul li a'); if ($lessons.length) { $imgIcon = $('<img src="'+toRoot+'assets/images/resource-tutorial.png" ' + ' width="64" height="64" alt=""/>'); $lessons.each(function(index) { $olLessons.append('<li><a href="'+$(this).attr('href')+'">' + $(this).html()+'</a></li>'); }); } else { $imgIcon = $('<img src="'+toRoot+'assets/images/resource-article.png" ' + ' width="64" height="64" alt=""/>'); $pSummary.addClass('article'); } $liClass.append($h2Title).append($imgIcon).append($pSummary).append($olLessons); $olClasses.append($liClass); }); $('.jd-descr').append($olClasses); } // Set up expand/collapse behavior initExpandableNavItems("#nav"); $(".scroll-pane").scroll(function(event) { event.preventDefault(); return false; }); /* Resize nav height when window height changes */ $(window).resize(function() { if ($('#side-nav').length == 0) return; var stylesheet = $('link[rel="stylesheet"][class="fullscreen"]'); setNavBarLeftPos(); // do this even if sidenav isn't fixed because it could become fixed // make sidenav behave when resizing the window and side-scolling is a concern if (sticky) { if ((stylesheet.attr("disabled") == "disabled") || stylesheet.length == 0) { updateSideNavPosition(); } else { updateSidenavFullscreenWidth(); } } resizeNav(); }); var navBarLeftPos; if ($('#devdoc-nav').length) { setNavBarLeftPos(); } // Set up play-on-hover <video> tags. $('video.play-on-hover').bind('click', function(){ $(this).get(0).load(); // in case the video isn't seekable $(this).get(0).play(); }); // Set up tooltips var TOOLTIP_MARGIN = 10; $('acronym,.tooltip-link').each(function() { var $target = $(this); var $tooltip = $('<div>') .addClass('tooltip-box') .append($target.attr('title')) .hide() .appendTo('body'); $target.removeAttr('title'); $target.hover(function() { // in var targetRect = $target.offset(); targetRect.width = $target.width(); targetRect.height = $target.height(); $tooltip.css({ left: targetRect.left, top: targetRect.top + targetRect.height + TOOLTIP_MARGIN }); $tooltip.addClass('below'); $tooltip.show(); }, function() { // out $tooltip.hide(); }); }); // Set up <h2> deeplinks $('h2').click(function() { var id = $(this).attr('id'); if (id) { document.location.hash = id; } }); //Loads the +1 button var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true; po.src = 'https://apis.google.com/js/plusone.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); // Revise the sidenav widths to make room for the scrollbar // which avoids the visible width from changing each time the bar appears var $sidenav = $("#side-nav"); var sidenav_width = parseInt($sidenav.innerWidth()); $("#devdoc-nav #nav").css("width", sidenav_width - 4 + "px"); // 4px is scrollbar width $(".scroll-pane").removeAttr("tabindex"); // get rid of tabindex added by jscroller if ($(".scroll-pane").length > 1) { // Check if there's a user preference for the panel heights var cookieHeight = readCookie("reference_height"); if (cookieHeight) { restoreHeight(cookieHeight); } } // Resize once loading is finished resizeNav(); // Check if there's an anchor that we need to scroll into view. // A delay is needed, because some browsers do not immediately scroll down to the anchor window.setTimeout(offsetScrollForSticky, 100); /* init the language selector based on user cookie for lang */ loadLangPref(); changeNavLang(getLangPref()); /* setup event handlers to ensure the overflow menu is visible while picking lang */ $("#language select") .mousedown(function() { $("div.morehover").addClass("hover"); }) .blur(function() { $("div.morehover").removeClass("hover"); }); /* some global variable setup */ resizePackagesNav = $("#resize-packages-nav"); classesNav = $("#classes-nav"); devdocNav = $("#devdoc-nav"); var cookiePath = ""; if (location.href.indexOf("/reference/") != -1) { cookiePath = "reference_"; } else if (location.href.indexOf("/guide/") != -1) { cookiePath = "guide_"; } else if (location.href.indexOf("/tools/") != -1) { cookiePath = "tools_"; } else if (location.href.indexOf("/training/") != -1) { cookiePath = "training_"; } else if (location.href.indexOf("/design/") != -1) { cookiePath = "design_"; } else if (location.href.indexOf("/distribute/") != -1) { cookiePath = "distribute_"; } /* setup shadowbox for any videos that want it */ var $videoLinks = $("a.video-shadowbox-button, a.notice-developers-video"); if ($videoLinks.length) { // if there's at least one, add the shadowbox HTML to the body $('body').prepend( '<div id="video-container">'+ '<div id="video-frame">'+ '<div class="video-close">'+ '<span id="icon-video-close" onclick="closeVideo()"> </span>'+ '</div>'+ '<div id="youTubePlayer"></div>'+ '</div>'+ '</div>'); // loads the IFrame Player API code asynchronously. $.getScript("https://www.youtube.com/iframe_api"); $videoLinks.each(function() { var videoId = $(this).attr('href').split('?v=')[1]; $(this).click(function(event) { event.preventDefault(); startYouTubePlayer(videoId); }); }); } }); // END of the onload event var youTubePlayer; function onYouTubeIframeAPIReady() { } /* Returns the height the shadowbox video should be. It's based on the current height of the "video-frame" element, which is 100% height for the window. Then minus the margin so the video isn't actually the full window height. */ function getVideoHeight() { var frameHeight = $("#video-frame").height(); var marginTop = $("#video-frame").css('margin-top').split('px')[0]; return frameHeight - (marginTop * 2); } var mPlayerPaused = false; function startYouTubePlayer(videoId) { $("#video-container").show(); $("#video-frame").show(); mPlayerPaused = false; // compute the size of the player so it's centered in window var maxWidth = 940; // the width of the web site content var videoAspect = .5625; // based on 1280x720 resolution var maxHeight = maxWidth * videoAspect; var videoHeight = getVideoHeight(); var videoWidth = videoHeight / videoAspect; if (videoWidth > maxWidth) { videoWidth = maxWidth; videoHeight = maxHeight; } $("#video-frame").css('width', videoWidth); // check if we've already created this player if (youTubePlayer == null) { // check if there's a start time specified var idAndHash = videoId.split("#"); var startTime = 0; if (idAndHash.length > 1) { startTime = idAndHash[1].split("t=")[1] != undefined ? idAndHash[1].split("t=")[1] : 0; } // enable localized player var lang = getLangPref(); var captionsOn = lang == 'en' ? 0 : 1; youTubePlayer = new YT.Player('youTubePlayer', { height: videoHeight, width: videoWidth, videoId: idAndHash[0], playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn}, events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange } }); } else { // reset the size in case the user adjusted the window since last play youTubePlayer.setSize(videoWidth, videoHeight); // if a video different from the one already playing was requested, cue it up if (videoId != youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0]) { youTubePlayer.cueVideoById(videoId); } youTubePlayer.playVideo(); } } function onPlayerReady(event) { event.target.playVideo(); mPlayerPaused = false; } function closeVideo() { try { youTubePlayer.pauseVideo(); } catch(e) { } $("#video-container").fadeOut(200); } /* Track youtube playback for analytics */ function onPlayerStateChange(event) { // Video starts, send the video ID if (event.data == YT.PlayerState.PLAYING) { if (mPlayerPaused) { ga('send', 'event', 'Videos', 'Resume', youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0]); } else { // track the start playing event so we know from which page the video was selected ga('send', 'event', 'Videos', 'Start: ' + youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0], 'on: ' + document.location.href); } mPlayerPaused = false; } // Video paused, send video ID and video elapsed time if (event.data == YT.PlayerState.PAUSED) { ga('send', 'event', 'Videos', 'Paused', youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0], youTubePlayer.getCurrentTime()); mPlayerPaused = true; } // Video finished, send video ID and video elapsed time if (event.data == YT.PlayerState.ENDED) { ga('send', 'event', 'Videos', 'Finished', youTubePlayer.getVideoUrl().split('?v=')[1].split('&')[0].split('%')[0], youTubePlayer.getCurrentTime()); mPlayerPaused = true; } } function initExpandableNavItems(rootTag) { $(rootTag + ' li.nav-section .nav-section-header').click(function() { var section = $(this).closest('li.nav-section'); if (section.hasClass('expanded')) { /* hide me and descendants */ section.find('ul').slideUp(250, function() { // remove 'expanded' class from my section and any children section.closest('li').removeClass('expanded'); $('li.nav-section', section).removeClass('expanded'); resizeNav(); }); } else { /* show me */ // first hide all other siblings var $others = $('li.nav-section.expanded', $(this).closest('ul')).not('.sticky'); $others.removeClass('expanded').children('ul').slideUp(250); // now expand me section.closest('li').addClass('expanded'); section.children('ul').slideDown(250, function() { resizeNav(); }); } }); // Stop expand/collapse behavior when clicking on nav section links // (since we're navigating away from the page) // This selector captures the first instance of <a>, but not those with "#" as the href. $('.nav-section-header').find('a:eq(0)').not('a[href="#"]').click(function(evt) { window.location.href = $(this).attr('href'); return false; }); } /** Create the list of breadcrumb links in the sticky header */ function buildBreadcrumbs() { var $breadcrumbUl = $("#sticky-header ul.breadcrumb"); // Add the secondary horizontal nav item, if provided var $selectedSecondNav = $("div#nav-x ul.nav-x a.selected").clone().removeClass("selected"); if ($selectedSecondNav.length) { $breadcrumbUl.prepend($("<li>").append($selectedSecondNav)) } // Add the primary horizontal nav var $selectedFirstNav = $("div#header-wrap ul.nav-x a.selected").clone().removeClass("selected"); // If there's no header nav item, use the logo link and title from alt text if ($selectedFirstNav.length < 1) { $selectedFirstNav = $("<a>") .attr('href', $("div#header .logo a").attr('href')) .text($("div#header .logo img").attr('alt')); } $breadcrumbUl.prepend($("<li>").append($selectedFirstNav)); } /** Highlight the current page in sidenav, expanding children as appropriate */ function highlightSidenav() { // if something is already highlighted, undo it. This is for dynamic navigation (Samples index) if ($("ul#nav li.selected").length) { unHighlightSidenav(); } // look for URL in sidenav, including the hash var $selNavLink = $('#nav').find('a[href="' + mPagePath + location.hash + '"]'); // If the selNavLink is still empty, look for it without the hash if ($selNavLink.length == 0) { $selNavLink = $('#nav').find('a[href="' + mPagePath + '"]'); } var $selListItem; if ($selNavLink.length) { // Find this page's <li> in sidenav and set selected $selListItem = $selNavLink.closest('li'); $selListItem.addClass('selected'); // Traverse up the tree and expand all parent nav-sections $selNavLink.parents('li.nav-section').each(function() { $(this).addClass('expanded'); $(this).children('ul').show(); }); } } function unHighlightSidenav() { $("ul#nav li.selected").removeClass("selected"); $('ul#nav li.nav-section.expanded').removeClass('expanded').children('ul').hide(); } function toggleFullscreen(enable) { var delay = 20; var enabled = true; var stylesheet = $('link[rel="stylesheet"][class="fullscreen"]'); if (enable) { // Currently NOT USING fullscreen; enable fullscreen stylesheet.removeAttr('disabled'); $('#nav-swap .fullscreen').removeClass('disabled'); $('#devdoc-nav').css({left:''}); setTimeout(updateSidenavFullscreenWidth,delay); // need to wait a moment for css to switch enabled = true; } else { // Currently USING fullscreen; disable fullscreen stylesheet.attr('disabled', 'disabled'); $('#nav-swap .fullscreen').addClass('disabled'); setTimeout(updateSidenavFixedWidth,delay); // need to wait a moment for css to switch enabled = false; } writeCookie("fullscreen", enabled, null); setNavBarLeftPos(); resizeNav(delay); updateSideNavPosition(); setTimeout(initSidenavHeightResize,delay); } function setNavBarLeftPos() { navBarLeftPos = $('#body-content').offset().left; } function updateSideNavPosition() { var newLeft = $(window).scrollLeft() - navBarLeftPos; $('#devdoc-nav').css({left: -newLeft}); $('#devdoc-nav .totop').css({left: -(newLeft - parseInt($('#side-nav').css('margin-left')))}); } // TODO: use $(document).ready instead function addLoadEvent(newfun) { var current = window.onload; if (typeof window.onload != 'function') { window.onload = newfun; } else { window.onload = function() { current(); newfun(); } } } var agent = navigator['userAgent'].toLowerCase(); // If a mobile phone, set flag and do mobile setup if ((agent.indexOf("mobile") != -1) || // android, iphone, ipod (agent.indexOf("blackberry") != -1) || (agent.indexOf("webos") != -1) || (agent.indexOf("mini") != -1)) { // opera mini browsers isMobile = true; } $(document).ready(function() { $("pre:not(.no-pretty-print)").addClass("prettyprint"); prettyPrint(); }); /* ######### RESIZE THE SIDENAV HEIGHT ########## */ function resizeNav(delay) { var $nav = $("#devdoc-nav"); var $window = $(window); var navHeight; // Get the height of entire window and the total header height. // Then figure out based on scroll position whether the header is visible var windowHeight = $window.height(); var scrollTop = $window.scrollTop(); var headerHeight = $('#header-wrapper').outerHeight(); var headerVisible = scrollTop < stickyTop; // get the height of space between nav and top of window. // Could be either margin or top position, depending on whether the nav is fixed. var topMargin = (parseInt($nav.css('margin-top')) || parseInt($nav.css('top'))) + 1; // add 1 for the #side-nav bottom margin // Depending on whether the header is visible, set the side nav's height. if (headerVisible) { // The sidenav height grows as the header goes off screen navHeight = windowHeight - (headerHeight - scrollTop) - topMargin; } else { // Once header is off screen, the nav height is almost full window height navHeight = windowHeight - topMargin; } $scrollPanes = $(".scroll-pane"); if ($scrollPanes.length > 1) { // subtract the height of the api level widget and nav swapper from the available nav height navHeight -= ($('#api-nav-header').outerHeight(true) + $('#nav-swap').outerHeight(true)); $("#swapper").css({height:navHeight + "px"}); if ($("#nav-tree").is(":visible")) { $("#nav-tree").css({height:navHeight}); } var classesHeight = navHeight - parseInt($("#resize-packages-nav").css("height")) - 10 + "px"; //subtract 10px to account for drag bar // if the window becomes small enough to make the class panel height 0, // then the package panel should begin to shrink if (parseInt(classesHeight) <= 0) { $("#resize-packages-nav").css({height:navHeight - 10}); //subtract 10px for drag bar $("#packages-nav").css({height:navHeight - 10}); } $("#classes-nav").css({'height':classesHeight, 'margin-top':'10px'}); $("#classes-nav .jspContainer").css({height:classesHeight}); } else { $nav.height(navHeight); } if (delay) { updateFromResize = true; delayedReInitScrollbars(delay); } else { reInitScrollbars(); } } var updateScrollbars = false; var updateFromResize = false; /* Re-initialize the scrollbars to account for changed nav size. * This method postpones the actual update by a 1/4 second in order to optimize the * scroll performance while the header is still visible, because re-initializing the * scroll panes is an intensive process. */ function delayedReInitScrollbars(delay) { // If we're scheduled for an update, but have received another resize request // before the scheduled resize has occured, just ignore the new request // (and wait for the scheduled one). if (updateScrollbars && updateFromResize) { updateFromResize = false; return; } // We're scheduled for an update and the update request came from this method's setTimeout if (updateScrollbars && !updateFromResize) { reInitScrollbars(); updateScrollbars = false; } else { updateScrollbars = true; updateFromResize = false; setTimeout('delayedReInitScrollbars()',delay); } } /* Re-initialize the scrollbars to account for changed nav size. */ function reInitScrollbars() { var pane = $(".scroll-pane").each(function(){ var api = $(this).data('jsp'); if (!api) { setTimeout(reInitScrollbars,300); return;} api.reinitialise( {verticalGutter:0} ); }); $(".scroll-pane").removeAttr("tabindex"); // get rid of tabindex added by jscroller } /* Resize the height of the nav panels in the reference, * and save the new size to a cookie */ function saveNavPanels() { var basePath = getBaseUri(location.pathname); var section = basePath.substring(1,basePath.indexOf("/",1)); writeCookie("height", resizePackagesNav.css("height"), section); } function restoreHeight(packageHeight) { $("#resize-packages-nav").height(packageHeight); $("#packages-nav").height(packageHeight); // var classesHeight = navHeight - packageHeight; // $("#classes-nav").css({height:classesHeight}); // $("#classes-nav .jspContainer").css({height:classesHeight}); } /* ######### END RESIZE THE SIDENAV HEIGHT ########## */ /** Scroll the jScrollPane to make the currently selected item visible This is called when the page finished loading. */ function scrollIntoView(nav) { var $nav = $("#"+nav); var element = $nav.jScrollPane({/* ...settings... */}); var api = element.data('jsp'); if ($nav.is(':visible')) { var $selected = $(".selected", $nav); if ($selected.length == 0) { // If no selected item found, exit return; } // get the selected item's offset from its container nav by measuring the item's offset // relative to the document then subtract the container nav's offset relative to the document var selectedOffset = $selected.offset().top - $nav.offset().top; if (selectedOffset > $nav.height() * .8) { // multiply nav height by .8 so we move up the item // if it's more than 80% down the nav // scroll the item up by an amount equal to 80% the container nav's height api.scrollTo(0, selectedOffset - ($nav.height() * .8), false); } } } /* Show popup dialogs */ function showDialog(id) { $dialog = $("#"+id); $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>'); $dialog.wrapInner('<div/>'); $dialog.removeClass("hide"); } /* ######### COOKIES! ########## */ function readCookie(cookie) { var myCookie = cookie_namespace+"_"+cookie+"="; if (document.cookie) { var index = document.cookie.indexOf(myCookie); if (index != -1) { var valStart = index + myCookie.length; var valEnd = document.cookie.indexOf(";", valStart); if (valEnd == -1) { valEnd = document.cookie.length; } var val = document.cookie.substring(valStart, valEnd); return val; } } return 0; } function writeCookie(cookie, val, section) { if (val==undefined) return; section = section == null ? "_" : "_"+section+"_"; var age = 2*365*24*60*60; // set max-age to 2 years var cookieValue = cookie_namespace + section + cookie + "=" + val + "; max-age=" + age +"; path=/"; document.cookie = cookieValue; } /* ######### END COOKIES! ########## */ var sticky = false; var stickyTop; var prevScrollLeft = 0; // used to compare current position to previous position of horiz scroll /* Sets the vertical scoll position at which the sticky bar should appear. This method is called to reset the position when search results appear or hide */ function setStickyTop() { stickyTop = $('#header-wrapper').outerHeight() - $('#sticky-header').outerHeight(); } /* * Displays sticky nav bar on pages when dac header scrolls out of view */ $(window).scroll(function(event) { setStickyTop(); var hiding = false; var $stickyEl = $('#sticky-header'); var $menuEl = $('.menu-container'); // Exit if there's no sidenav if ($('#side-nav').length == 0) return; // Exit if the mouse target is a DIV, because that means the event is coming // from a scrollable div and so there's no need to make adjustments to our layout if ($(event.target).nodeName == "DIV") { return; } var top = $(window).scrollTop(); // we set the navbar fixed when the scroll position is beyond the height of the site header... var shouldBeSticky = top >= stickyTop; // ... except if the document content is shorter than the sidenav height. // (this is necessary to avoid crazy behavior on OSX Lion due to overscroll bouncing) if ($("#doc-col").height() < $("#side-nav").height()) { shouldBeSticky = false; } // Account for horizontal scroll var scrollLeft = $(window).scrollLeft(); // When the sidenav is fixed and user scrolls horizontally, reposition the sidenav to match if (sticky && (scrollLeft != prevScrollLeft)) { updateSideNavPosition(); prevScrollLeft = scrollLeft; } // Don't continue if the header is sufficently far away // (to avoid intensive resizing that slows scrolling) if (sticky == shouldBeSticky) { return; } // If sticky header visible and position is now near top, hide sticky if (sticky && !shouldBeSticky) { sticky = false; hiding = true; // make the sidenav static again $('#devdoc-nav') .removeClass('fixed') .css({'width':'auto','margin':''}) .prependTo('#side-nav'); // delay hide the sticky $menuEl.removeClass('sticky-menu'); $stickyEl.fadeOut(250); hiding = false; // update the sidenaav position for side scrolling updateSideNavPosition(); } else if (!sticky && shouldBeSticky) { sticky = true; $stickyEl.fadeIn(10); $menuEl.addClass('sticky-menu'); // make the sidenav fixed var width = $('#devdoc-nav').width(); $('#devdoc-nav') .addClass('fixed') .css({'width':width+'px'}) .prependTo('#body-content'); // update the sidenaav position for side scrolling updateSideNavPosition(); } else if (hiding && top < 15) { $menuEl.removeClass('sticky-menu'); $stickyEl.hide(); hiding = false; } resizeNav(250); // pass true in order to delay the scrollbar re-initialization for performance }); /* * Manages secion card states and nav resize to conclude loading */ (function() { $(document).ready(function() { // Stack hover states $('.section-card-menu').each(function(index, el) { var height = $(el).height(); $(el).css({height:height+'px', position:'relative'}); var $cardInfo = $(el).find('.card-info'); $cardInfo.css({position: 'absolute', bottom:'0px', left:'0px', right:'0px', overflow:'visible'}); }); }); })(); /* MISC LIBRARY FUNCTIONS */ function toggle(obj, slide) { var ul = $("ul:first", obj); var li = ul.parent(); if (li.hasClass("closed")) { if (slide) { ul.slideDown("fast"); } else { ul.show(); } li.removeClass("closed"); li.addClass("open"); $(".toggle-img", li).attr("title", "hide pages"); } else { ul.slideUp("fast"); li.removeClass("open"); li.addClass("closed"); $(".toggle-img", li).attr("title", "show pages"); } } function buildToggleLists() { $(".toggle-list").each( function(i) { $("div:first", this).append("<a class='toggle-img' href='#' title='show pages' onClick='toggle(this.parentNode.parentNode, true); return false;'></a>"); $(this).addClass("closed"); }); } function hideNestedItems(list, toggle) { $list = $(list); // hide nested lists if($list.hasClass('showing')) { $("li ol", $list).hide('fast'); $list.removeClass('showing'); // show nested lists } else { $("li ol", $list).show('fast'); $list.addClass('showing'); } $(".more,.less",$(toggle)).toggle(); } /* Call this to add listeners to a <select> element for Studio/Eclipse/Other docs */ function setupIdeDocToggle() { $( "select.ide" ).change(function() { var selected = $(this).find("option:selected").attr("value"); $(".select-ide").hide(); $(".select-ide."+selected).show(); $("select.ide").val(selected); }); } /* REFERENCE NAV SWAP */ function getNavPref() { var v = readCookie('reference_nav'); if (v != NAV_PREF_TREE) { v = NAV_PREF_PANELS; } return v; } function chooseDefaultNav() { nav_pref = getNavPref(); if (nav_pref == NAV_PREF_TREE) { $("#nav-panels").toggle(); $("#panel-link").toggle(); $("#nav-tree").toggle(); $("#tree-link").toggle(); } } function swapNav() { if (nav_pref == NAV_PREF_TREE) { nav_pref = NAV_PREF_PANELS; } else { nav_pref = NAV_PREF_TREE; init_default_navtree(toRoot); } writeCookie("nav", nav_pref, "reference"); $("#nav-panels").toggle(); $("#panel-link").toggle(); $("#nav-tree").toggle(); $("#tree-link").toggle(); resizeNav(); // Gross nasty hack to make tree view show up upon first swap by setting height manually $("#nav-tree .jspContainer:visible") .css({'height':$("#nav-tree .jspContainer .jspPane").height() +'px'}); // Another nasty hack to make the scrollbar appear now that we have height resizeNav(); if ($("#nav-tree").is(':visible')) { scrollIntoView("nav-tree"); } else { scrollIntoView("packages-nav"); scrollIntoView("classes-nav"); } } /* ############################################ */ /* ########## LOCALIZATION ############ */ /* ############################################ */ function getBaseUri(uri) { var intlUrl = (uri.substring(0,6) == "/intl/"); if (intlUrl) { base = uri.substring(uri.indexOf('intl/')+5,uri.length); base = base.substring(base.indexOf('/')+1, base.length); //alert("intl, returning base url: /" + base); return ("/" + base); } else { //alert("not intl, returning uri as found."); return uri; } } function requestAppendHL(uri) { //append "?hl=<lang> to an outgoing request (such as to blog) var lang = getLangPref(); if (lang) { var q = 'hl=' + lang; uri += '?' + q; window.location = uri; return false; } else { return true; } } function changeNavLang(lang) { var $links = $("#devdoc-nav,#header,#nav-x,.training-nav-top,.content-footer").find("a["+lang+"-lang]"); $links.each(function(i){ // for each link with a translation var $link = $(this); if (lang != "en") { // No need to worry about English, because a language change invokes new request // put the desired language from the attribute as the text $link.text($link.attr(lang+"-lang")) } }); } function changeLangPref(lang, submit) { writeCookie("pref_lang", lang, null); // ####### TODO: Remove this condition once we're stable on devsite ####### // This condition is only needed if we still need to support legacy GAE server if (devsite) { // Switch language when on Devsite server if (submit) { $("#setlang").submit(); } } else { // Switch language when on legacy GAE server if (submit) { window.location = getBaseUri(location.pathname); } } } function loadLangPref() { var lang = readCookie("pref_lang"); if (lang != 0) { $("#language").find("option[value='"+lang+"']").attr("selected",true); } } function getLangPref() { var lang = $("#language").find(":selected").attr("value"); if (!lang) { lang = readCookie("pref_lang"); } return (lang != 0) ? lang : 'en'; } /* ########## END LOCALIZATION ############ */ /* Used to hide and reveal supplemental content, such as long code samples. See the companion CSS in android-developer-docs.css */ function toggleContent(obj) { var div = $(obj).closest(".toggle-content"); var toggleMe = $(".toggle-content-toggleme:eq(0)",div); if (div.hasClass("closed")) { // if it's closed, open it toggleMe.slideDown(); $(".toggle-content-text:eq(0)", obj).toggle(); div.removeClass("closed").addClass("open"); $(".toggle-content-img:eq(0)", div).attr("title", "hide").attr("src", toRoot + "assets/images/triangle-opened.png"); } else { // if it's open, close it toggleMe.slideUp('fast', function() { // Wait until the animation is done before closing arrow $(".toggle-content-text:eq(0)", obj).toggle(); div.removeClass("open").addClass("closed"); div.find(".toggle-content").removeClass("open").addClass("closed") .find(".toggle-content-toggleme").hide(); $(".toggle-content-img", div).attr("title", "show").attr("src", toRoot + "assets/images/triangle-closed.png"); }); } return false; } /* New version of expandable content */ function toggleExpandable(link,id) { if($(id).is(':visible')) { $(id).slideUp(); $(link).removeClass('expanded'); } else { $(id).slideDown(); $(link).addClass('expanded'); } } function hideExpandable(ids) { $(ids).slideUp(); $(ids).prev('h4').find('a.expandable').removeClass('expanded'); } /* * Slideshow 1.0 * Used on /index.html and /develop/index.html for carousel * * Sample usage: * HTML - * <div class="slideshow-container"> * <a href="" class="slideshow-prev">Prev</a> * <a href="" class="slideshow-next">Next</a> * <ul> * <li class="item"><img src="images/marquee1.jpg"></li> * <li class="item"><img src="images/marquee2.jpg"></li> * <li class="item"><img src="images/marquee3.jpg"></li> * <li class="item"><img src="images/marquee4.jpg"></li> * </ul> * </div> * * <script type="text/javascript"> * $('.slideshow-container').dacSlideshow({ * auto: true, * btnPrev: '.slideshow-prev', * btnNext: '.slideshow-next' * }); * </script> * * Options: * btnPrev: optional identifier for previous button * btnNext: optional identifier for next button * btnPause: optional identifier for pause button * auto: whether or not to auto-proceed * speed: animation speed * autoTime: time between auto-rotation * easing: easing function for transition * start: item to select by default * scroll: direction to scroll in * pagination: whether or not to include dotted pagination * */ (function($) { $.fn.dacSlideshow = function(o) { //Options - see above o = $.extend({ btnPrev: null, btnNext: null, btnPause: null, auto: true, speed: 500, autoTime: 12000, easing: null, start: 0, scroll: 1, pagination: true }, o || {}); //Set up a carousel for each return this.each(function() { var running = false; var animCss = o.vertical ? "top" : "left"; var sizeCss = o.vertical ? "height" : "width"; var div = $(this); var ul = $("ul", div); var tLi = $("li", ul); var tl = tLi.size(); var timer = null; var li = $("li", ul); var itemLength = li.size(); var curr = o.start; li.css({float: o.vertical ? "none" : "left"}); ul.css({margin: "0", padding: "0", position: "relative", "list-style-type": "none", "z-index": "1"}); div.css({position: "relative", "z-index": "2", left: "0px"}); var liSize = o.vertical ? height(li) : width(li); var ulSize = liSize * itemLength; var divSize = liSize; li.css({width: li.width(), height: li.height()}); ul.css(sizeCss, ulSize+"px").css(animCss, -(curr*liSize)); div.css(sizeCss, divSize+"px"); //Pagination if (o.pagination) { var pagination = $("<div class='pagination'></div>"); var pag_ul = $("<ul></ul>"); if (tl > 1) { for (var i=0;i<tl;i++) { var li = $("<li>"+i+"</li>"); pag_ul.append(li); if (i==o.start) li.addClass('active'); li.click(function() { go(parseInt($(this).text())); }) } pagination.append(pag_ul); div.append(pagination); } } //Previous button if(o.btnPrev) $(o.btnPrev).click(function(e) { e.preventDefault(); return go(curr-o.scroll); }); //Next button if(o.btnNext) $(o.btnNext).click(function(e) { e.preventDefault(); return go(curr+o.scroll); }); //Pause button if(o.btnPause) $(o.btnPause).click(function(e) { e.preventDefault(); if ($(this).hasClass('paused')) { startRotateTimer(); } else { pauseRotateTimer(); } }); //Auto rotation if(o.auto) startRotateTimer(); function startRotateTimer() { clearInterval(timer); timer = setInterval(function() { if (curr == tl-1) { go(0); } else { go(curr+o.scroll); } }, o.autoTime); $(o.btnPause).removeClass('paused'); } function pauseRotateTimer() { clearInterval(timer); $(o.btnPause).addClass('paused'); } //Go to an item function go(to) { if(!running) { if(to<0) { to = itemLength-1; } else if (to>itemLength-1) { to = 0; } curr = to; running = true; ul.animate( animCss == "left" ? { left: -(curr*liSize) } : { top: -(curr*liSize) } , o.speed, o.easing, function() { running = false; } ); $(o.btnPrev + "," + o.btnNext).removeClass("disabled"); $( (curr-o.scroll<0 && o.btnPrev) || (curr+o.scroll > itemLength && o.btnNext) || [] ).addClass("disabled"); var nav_items = $('li', pagination); nav_items.removeClass('active'); nav_items.eq(to).addClass('active'); } if(o.auto) startRotateTimer(); return false; }; }); }; function css(el, prop) { return parseInt($.css(el[0], prop)) || 0; }; function width(el) { return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight'); }; function height(el) { return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom'); }; })(jQuery); /* * dacSlideshow 1.0 * Used on develop/index.html for side-sliding tabs * * Sample usage: * HTML - * <div class="slideshow-container"> * <a href="" class="slideshow-prev">Prev</a> * <a href="" class="slideshow-next">Next</a> * <ul> * <li class="item"><img src="images/marquee1.jpg"></li> * <li class="item"><img src="images/marquee2.jpg"></li> * <li class="item"><img src="images/marquee3.jpg"></li> * <li class="item"><img src="images/marquee4.jpg"></li> * </ul> * </div> * * <script type="text/javascript"> * $('.slideshow-container').dacSlideshow({ * auto: true, * btnPrev: '.slideshow-prev', * btnNext: '.slideshow-next' * }); * </script> * * Options: * btnPrev: optional identifier for previous button * btnNext: optional identifier for next button * auto: whether or not to auto-proceed * speed: animation speed * autoTime: time between auto-rotation * easing: easing function for transition * start: item to select by default * scroll: direction to scroll in * pagination: whether or not to include dotted pagination * */ (function($) { $.fn.dacTabbedList = function(o) { //Options - see above o = $.extend({ speed : 250, easing: null, nav_id: null, frame_id: null }, o || {}); //Set up a carousel for each return this.each(function() { var curr = 0; var running = false; var animCss = "margin-left"; var sizeCss = "width"; var div = $(this); var nav = $(o.nav_id, div); var nav_li = $("li", nav); var nav_size = nav_li.size(); var frame = div.find(o.frame_id); var content_width = $(frame).find('ul').width(); //Buttons $(nav_li).click(function(e) { go($(nav_li).index($(this))); }) //Go to an item function go(to) { if(!running) { curr = to; running = true; frame.animate({ 'margin-left' : -(curr*content_width) }, o.speed, o.easing, function() { running = false; } ); nav_li.removeClass('active'); nav_li.eq(to).addClass('active'); } return false; }; }); }; function css(el, prop) { return parseInt($.css(el[0], prop)) || 0; }; function width(el) { return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight'); }; function height(el) { return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom'); }; })(jQuery); /* ######################################################## */ /* ################ SEARCH SUGGESTIONS ################## */ /* ######################################################## */ var gSelectedIndex = -1; // the index position of currently highlighted suggestion var gSelectedColumn = -1; // which column of suggestion lists is currently focused var gMatches = new Array(); var gLastText = ""; var gInitialized = false; var ROW_COUNT_FRAMEWORK = 20; // max number of results in list var gListLength = 0; var gGoogleMatches = new Array(); var ROW_COUNT_GOOGLE = 15; // max number of results in list var gGoogleListLength = 0; var gDocsMatches = new Array(); var ROW_COUNT_DOCS = 100; // max number of results in list var gDocsListLength = 0; function onSuggestionClick(link) { // When user clicks a suggested document, track it ga('send', 'event', 'Suggestion Click', 'clicked: ' + $(link).attr('href'), 'query: ' + $("#search_autocomplete").val().toLowerCase()); } function set_item_selected($li, selected) { if (selected) { $li.attr('class','jd-autocomplete jd-selected'); } else { $li.attr('class','jd-autocomplete'); } } function set_item_values(toroot, $li, match) { var $link = $('a',$li); $link.html(match.__hilabel || match.label); $link.attr('href',toroot + match.link); } function set_item_values_jd(toroot, $li, match) { var $link = $('a',$li); $link.html(match.title); $link.attr('href',toroot + match.url); } function new_suggestion($list) { var $li = $("<li class='jd-autocomplete'></li>"); $list.append($li); $li.mousedown(function() { window.location = this.firstChild.getAttribute("href"); }); $li.mouseover(function() { $('.search_filtered_wrapper li').removeClass('jd-selected'); $(this).addClass('jd-selected'); gSelectedColumn = $(".search_filtered:visible").index($(this).closest('.search_filtered')); gSelectedIndex = $("li", $(".search_filtered:visible")[gSelectedColumn]).index(this); }); $li.append("<a onclick='onSuggestionClick(this)'></a>"); $li.attr('class','show-item'); return $li; } function sync_selection_table(toroot) { var $li; //list item jquery object var i; //list item iterator // if there are NO results at all, hide all columns if (!(gMatches.length > 0) && !(gGoogleMatches.length > 0) && !(gDocsMatches.length > 0)) { $('.suggest-card').hide(300); return; } // if there are api results if ((gMatches.length > 0) || (gGoogleMatches.length > 0)) { // reveal suggestion list $('.suggest-card.dummy').show(); $('.suggest-card.reference').show(); var listIndex = 0; // list index position // reset the lists $(".search_filtered_wrapper.reference li").remove(); // ########### ANDROID RESULTS ############# if (gMatches.length > 0) { // determine android results to show gListLength = gMatches.length < ROW_COUNT_FRAMEWORK ? gMatches.length : ROW_COUNT_FRAMEWORK; for (i=0; i<gListLength; i++) { var $li = new_suggestion($(".suggest-card.reference ul")); set_item_values(toroot, $li, gMatches[i]); set_item_selected($li, i == gSelectedIndex); } } // ########### GOOGLE RESULTS ############# if (gGoogleMatches.length > 0) { // show header for list $(".suggest-card.reference ul").append("<li class='header'>in Google Services:</li>"); // determine google results to show gGoogleListLength = gGoogleMatches.length < ROW_COUNT_GOOGLE ? gGoogleMatches.length : ROW_COUNT_GOOGLE; for (i=0; i<gGoogleListLength; i++) { var $li = new_suggestion($(".suggest-card.reference ul")); set_item_values(toroot, $li, gGoogleMatches[i]); set_item_selected($li, i == gSelectedIndex); } } } else { $('.suggest-card.reference').hide(); $('.suggest-card.dummy').hide(); } // ########### JD DOC RESULTS ############# if (gDocsMatches.length > 0) { // reset the lists $(".search_filtered_wrapper.docs li").remove(); // determine google results to show // NOTE: The order of the conditions below for the sugg.type MUST BE SPECIFIC: // The order must match the reverse order that each section appears as a card in // the suggestion UI... this may be only for the "develop" grouped items though. gDocsListLength = gDocsMatches.length < ROW_COUNT_DOCS ? gDocsMatches.length : ROW_COUNT_DOCS; for (i=0; i<gDocsListLength; i++) { var sugg = gDocsMatches[i]; var $li; if (sugg.type == "design") { $li = new_suggestion($(".suggest-card.design ul")); } else if (sugg.type == "distribute") { $li = new_suggestion($(".suggest-card.distribute ul")); } else if (sugg.type == "samples") { $li = new_suggestion($(".suggest-card.develop .child-card.samples")); } else if (sugg.type == "training") { $li = new_suggestion($(".suggest-card.develop .child-card.training")); } else if (sugg.type == "about"||"guide"||"tools"||"google") { $li = new_suggestion($(".suggest-card.develop .child-card.guides")); } else { continue; } set_item_values_jd(toroot, $li, sugg); set_item_selected($li, i == gSelectedIndex); } // add heading and show or hide card if ($(".suggest-card.design li").length > 0) { $(".suggest-card.design ul").prepend("<li class='header'>Design:</li>"); $(".suggest-card.design").show(300); } else { $('.suggest-card.design').hide(300); } if ($(".suggest-card.distribute li").length > 0) { $(".suggest-card.distribute ul").prepend("<li class='header'>Distribute:</li>"); $(".suggest-card.distribute").show(300); } else { $('.suggest-card.distribute').hide(300); } if ($(".child-card.guides li").length > 0) { $(".child-card.guides").prepend("<li class='header'>Guides:</li>"); $(".child-card.guides li").appendTo(".suggest-card.develop ul"); } if ($(".child-card.training li").length > 0) { $(".child-card.training").prepend("<li class='header'>Training:</li>"); $(".child-card.training li").appendTo(".suggest-card.develop ul"); } if ($(".child-card.samples li").length > 0) { $(".child-card.samples").prepend("<li class='header'>Samples:</li>"); $(".child-card.samples li").appendTo(".suggest-card.develop ul"); } if ($(".suggest-card.develop li").length > 0) { $(".suggest-card.develop").show(300); } else { $('.suggest-card.develop').hide(300); } } else { $('.search_filtered_wrapper.docs .suggest-card:not(.dummy)').hide(300); } } /** Called by the search input's onkeydown and onkeyup events. * Handles navigation with keyboard arrows, Enter key to invoke search, * otherwise invokes search suggestions on key-up event. * @param e The JS event * @param kd True if the event is key-down * @param toroot A string for the site's root path * @returns True if the event should bubble up */ function search_changed(e, kd, toroot) { var currentLang = getLangPref(); var search = document.getElementById("search_autocomplete"); var text = search.value.replace(/(^ +)|( +$)/g, ''); // get the ul hosting the currently selected item gSelectedColumn = gSelectedColumn >= 0 ? gSelectedColumn : 0; var $columns = $(".search_filtered_wrapper").find(".search_filtered:visible"); var $selectedUl = $columns[gSelectedColumn]; // show/hide the close button if (text != '') { $(".search .close").removeClass("hide"); } else { $(".search .close").addClass("hide"); } // 27 = esc if (e.keyCode == 27) { // close all search results if (kd) $('.search .close').trigger('click'); return true; } // 13 = enter else if (e.keyCode == 13) { if (gSelectedIndex < 0) { $('.suggest-card').hide(); if ($("#searchResults").is(":hidden") && (search.value != "")) { // if results aren't showing (and text not empty), return true to allow search to execute $('body,html').animate({scrollTop:0}, '500', 'swing'); return true; } else { // otherwise, results are already showing, so allow ajax to auto refresh the results // and ignore this Enter press to avoid the reload. return false; } } else if (kd && gSelectedIndex >= 0) { // click the link corresponding to selected item $("a",$("li",$selectedUl)[gSelectedIndex]).get()[0].click(); return false; } } // If Google results are showing, return true to allow ajax search to execute else if ($("#searchResults").is(":visible")) { // Also, if search_results is scrolled out of view, scroll to top to make results visible if ((sticky ) && (search.value != "")) { $('body,html').animate({scrollTop:0}, '500', 'swing'); } return true; } // 38 UP ARROW else if (kd && (e.keyCode == 38)) { // if the next item is a header, skip it if ($($("li", $selectedUl)[gSelectedIndex-1]).hasClass("header")) { gSelectedIndex--; } if (gSelectedIndex >= 0) { $('li', $selectedUl).removeClass('jd-selected'); gSelectedIndex--; $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected'); // If user reaches top, reset selected column if (gSelectedIndex < 0) { gSelectedColumn = -1; } } return false; } // 40 DOWN ARROW else if (kd && (e.keyCode == 40)) { // if the next item is a header, skip it if ($($("li", $selectedUl)[gSelectedIndex+1]).hasClass("header")) { gSelectedIndex++; } if ((gSelectedIndex < $("li", $selectedUl).length-1) || ($($("li", $selectedUl)[gSelectedIndex+1]).hasClass("header"))) { $('li', $selectedUl).removeClass('jd-selected'); gSelectedIndex++; $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected'); } return false; } // Consider left/right arrow navigation // NOTE: Order of suggest columns are reverse order (index position 0 is on right) else if (kd && $columns.length > 1 && gSelectedColumn >= 0) { // 37 LEFT ARROW // go left only if current column is not left-most column (last column) if (e.keyCode == 37 && gSelectedColumn < $columns.length - 1) { $('li', $selectedUl).removeClass('jd-selected'); gSelectedColumn++; $selectedUl = $columns[gSelectedColumn]; // keep or reset the selected item to last item as appropriate gSelectedIndex = gSelectedIndex > $("li", $selectedUl).length-1 ? $("li", $selectedUl).length-1 : gSelectedIndex; // if the corresponding item is a header, move down if ($($("li", $selectedUl)[gSelectedIndex]).hasClass("header")) { gSelectedIndex++; } // set item selected $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected'); return false; } // 39 RIGHT ARROW // go right only if current column is not the right-most column (first column) else if (e.keyCode == 39 && gSelectedColumn > 0) { $('li', $selectedUl).removeClass('jd-selected'); gSelectedColumn--; $selectedUl = $columns[gSelectedColumn]; // keep or reset the selected item to last item as appropriate gSelectedIndex = gSelectedIndex > $("li", $selectedUl).length-1 ? $("li", $selectedUl).length-1 : gSelectedIndex; // if the corresponding item is a header, move down if ($($("li", $selectedUl)[gSelectedIndex]).hasClass("header")) { gSelectedIndex++; } // set item selected $('li:nth-child('+(gSelectedIndex+1)+')', $selectedUl).addClass('jd-selected'); return false; } } // if key-up event and not arrow down/up/left/right, // read the search query and add suggestions to gMatches else if (!kd && (e.keyCode != 40) && (e.keyCode != 38) && (e.keyCode != 37) && (e.keyCode != 39)) { gSelectedIndex = -1; gMatches = new Array(); matchedCount = 0; gGoogleMatches = new Array(); matchedCountGoogle = 0; gDocsMatches = new Array(); matchedCountDocs = 0; // Search for Android matches for (var i=0; i<DATA.length; i++) { var s = DATA[i]; if (text.length != 0 && s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) { gMatches[matchedCount] = s; matchedCount++; } } rank_autocomplete_api_results(text, gMatches); for (var i=0; i<gMatches.length; i++) { var s = gMatches[i]; } // Search for Google matches for (var i=0; i<GOOGLE_DATA.length; i++) { var s = GOOGLE_DATA[i]; if (text.length != 0 && s.label.toLowerCase().indexOf(text.toLowerCase()) != -1) { gGoogleMatches[matchedCountGoogle] = s; matchedCountGoogle++; } } rank_autocomplete_api_results(text, gGoogleMatches); for (var i=0; i<gGoogleMatches.length; i++) { var s = gGoogleMatches[i]; } highlight_autocomplete_result_labels(text); // Search for matching JD docs if (text.length >= 2) { // Regex to match only the beginning of a word var textRegex = new RegExp("\\b" + text.toLowerCase(), "g"); // Search for Training classes for (var i=0; i<TRAINING_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = TRAINING_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Don't consider doc title for lessons (only for class landing pages), // unless the lesson has a tag that already matches if ((s.lang == currentLang) && (!(s.type == "training" && s.url.indexOf("index.html") == -1) || matched)) { // it matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for API Guides for (var i=0; i<GUIDE_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = GUIDE_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for Tools Guides for (var i=0; i<TOOLS_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = TOOLS_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for About docs for (var i=0; i<ABOUT_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = ABOUT_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for Design guides for (var i=0; i<DESIGN_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = DESIGN_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for Distribute guides for (var i=0; i<DISTRIBUTE_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = DISTRIBUTE_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for Google guides for (var i=0; i<GOOGLE_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = GOOGLE_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Search for Samples for (var i=0; i<SAMPLES_RESOURCES.length; i++) { // current search comparison, with counters for tag and title, // used later to improve ranking var s = SAMPLES_RESOURCES[i]; s.matched_tag = 0; s.matched_title = 0; var matched = false; // Check if query matches any tags; work backwards toward 1 to assist ranking for (var j = s.keywords.length - 1; j >= 0; j--) { // it matches a tag if (s.keywords[j].toLowerCase().match(textRegex)) { matched = true; s.matched_tag = j + 1; // add 1 to index position } } // Check if query matches the doc title, but only for current language if (s.lang == currentLang) { // if query matches the doc title.t if (s.title.toLowerCase().match(textRegex)) { matched = true; s.matched_title = 1; } } if (matched) { gDocsMatches[matchedCountDocs] = s; matchedCountDocs++; } } // Rank/sort all the matched pages rank_autocomplete_doc_results(text, gDocsMatches); } // draw the suggestions sync_selection_table(toroot); return true; // allow the event to bubble up to the search api } } /* Order the jd doc result list based on match quality */ function rank_autocomplete_doc_results(query, matches) { query = query || ''; if (!matches || !matches.length) return; var _resultScoreFn = function(match) { var score = 1.0; // if the query matched a tag if (match.matched_tag > 0) { // multiply score by factor relative to position in tags list (max of 3) score *= 3 / match.matched_tag; // if it also matched the title if (match.matched_title > 0) { score *= 2; } } else if (match.matched_title > 0) { score *= 3; } return score; }; for (var i=0; i<matches.length; i++) { matches[i].__resultScore = _resultScoreFn(matches[i]); } matches.sort(function(a,b){ var n = b.__resultScore - a.__resultScore; if (n == 0) // lexicographical sort if scores are the same n = (a.label < b.label) ? -1 : 1; return n; }); } /* Order the result list based on match quality */ function rank_autocomplete_api_results(query, matches) { query = query || ''; if (!matches || !matches.length) return; // helper function that gets the last occurence index of the given regex // in the given string, or -1 if not found var _lastSearch = function(s, re) { if (s == '') return -1; var l = -1; var tmp; while ((tmp = s.search(re)) >= 0) { if (l < 0) l = 0; l += tmp; s = s.substr(tmp + 1); } return l; }; // helper function that counts the occurrences of a given character in // a given string var _countChar = function(s, c) { var n = 0; for (var i=0; i<s.length; i++) if (s.charAt(i) == c) ++n; return n; }; var queryLower = query.toLowerCase(); var queryAlnum = (queryLower.match(/\w+/) || [''])[0]; var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum); var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b'); var _resultScoreFn = function(result) { // scores are calculated based on exact and prefix matches, // and then number of path separators (dots) from the last // match (i.e. favoring classes and deep package names) var score = 1.0; var labelLower = result.label.toLowerCase(); var t; t = _lastSearch(labelLower, partExactAlnumRE); if (t >= 0) { // exact part match var partsAfter = _countChar(labelLower.substr(t + 1), '.'); score *= 200 / (partsAfter + 1); } else { t = _lastSearch(labelLower, partPrefixAlnumRE); if (t >= 0) { // part prefix match var partsAfter = _countChar(labelLower.substr(t + 1), '.'); score *= 20 / (partsAfter + 1); } } return score; }; for (var i=0; i<matches.length; i++) { // if the API is deprecated, default score is 0; otherwise, perform scoring if (matches[i].deprecated == "true") { matches[i].__resultScore = 0; } else { matches[i].__resultScore = _resultScoreFn(matches[i]); } } matches.sort(function(a,b){ var n = b.__resultScore - a.__resultScore; if (n == 0) // lexicographical sort if scores are the same n = (a.label < b.label) ? -1 : 1; return n; }); } /* Add emphasis to part of string that matches query */ function highlight_autocomplete_result_labels(query) { query = query || ''; if ((!gMatches || !gMatches.length) && (!gGoogleMatches || !gGoogleMatches.length)) return; var queryLower = query.toLowerCase(); var queryAlnumDot = (queryLower.match(/[\w\.]+/) || [''])[0]; var queryRE = new RegExp( '(' + queryAlnumDot.replace(/\./g, '\\.') + ')', 'ig'); for (var i=0; i<gMatches.length; i++) { gMatches[i].__hilabel = gMatches[i].label.replace( queryRE, '<b>$1</b>'); } for (var i=0; i<gGoogleMatches.length; i++) { gGoogleMatches[i].__hilabel = gGoogleMatches[i].label.replace( queryRE, '<b>$1</b>'); } } function search_focus_changed(obj, focused) { if (!focused) { if(obj.value == ""){ $(".search .close").addClass("hide"); } $(".suggest-card").hide(); } } function submit_search() { var query = document.getElementById('search_autocomplete').value; location.hash = 'q=' + query; loadSearchResults(); $("#searchResults").slideDown('slow', setStickyTop); return false; } function hideResults() { $("#searchResults").slideUp('fast', setStickyTop); $(".search .close").addClass("hide"); location.hash = ''; $("#search_autocomplete").val("").blur(); // reset the ajax search callback to nothing, so results don't appear unless ENTER searchControl.setSearchStartingCallback(this, function(control, searcher, query) {}); // forcefully regain key-up event control (previously jacked by search api) $("#search_autocomplete").keyup(function(event) { return search_changed(event, false, toRoot); }); return false; } /* ########################################################## */ /* ################ CUSTOM SEARCH ENGINE ################## */ /* ########################################################## */ var searchControl; google.load('search', '1', {"callback" : function() { searchControl = new google.search.SearchControl(); } }); function loadSearchResults() { document.getElementById("search_autocomplete").style.color = "#000"; searchControl = new google.search.SearchControl(); // use our existing search form and use tabs when multiple searchers are used drawOptions = new google.search.DrawOptions(); drawOptions.setDrawMode(google.search.SearchControl.DRAW_MODE_TABBED); drawOptions.setInput(document.getElementById("search_autocomplete")); // configure search result options searchOptions = new google.search.SearcherOptions(); searchOptions.setExpandMode(GSearchControl.EXPAND_MODE_OPEN); // configure each of the searchers, for each tab devSiteSearcher = new google.search.WebSearch(); devSiteSearcher.setUserDefinedLabel("All"); devSiteSearcher.setSiteRestriction("001482626316274216503:zu90b7s047u"); designSearcher = new google.search.WebSearch(); designSearcher.setUserDefinedLabel("Design"); designSearcher.setSiteRestriction("http://developer.android.com/design/"); trainingSearcher = new google.search.WebSearch(); trainingSearcher.setUserDefinedLabel("Training"); trainingSearcher.setSiteRestriction("http://developer.android.com/training/"); guidesSearcher = new google.search.WebSearch(); guidesSearcher.setUserDefinedLabel("Guides"); guidesSearcher.setSiteRestriction("http://developer.android.com/guide/"); referenceSearcher = new google.search.WebSearch(); referenceSearcher.setUserDefinedLabel("Reference"); referenceSearcher.setSiteRestriction("http://developer.android.com/reference/"); googleSearcher = new google.search.WebSearch(); googleSearcher.setUserDefinedLabel("Google Services"); googleSearcher.setSiteRestriction("http://developer.android.com/google/"); blogSearcher = new google.search.WebSearch(); blogSearcher.setUserDefinedLabel("Blog"); blogSearcher.setSiteRestriction("http://android-developers.blogspot.com"); // add each searcher to the search control searchControl.addSearcher(devSiteSearcher, searchOptions); searchControl.addSearcher(designSearcher, searchOptions); searchControl.addSearcher(trainingSearcher, searchOptions); searchControl.addSearcher(guidesSearcher, searchOptions); searchControl.addSearcher(referenceSearcher, searchOptions); searchControl.addSearcher(googleSearcher, searchOptions); searchControl.addSearcher(blogSearcher, searchOptions); // configure result options searchControl.setResultSetSize(google.search.Search.LARGE_RESULTSET); searchControl.setLinkTarget(google.search.Search.LINK_TARGET_SELF); searchControl.setTimeoutInterval(google.search.SearchControl.TIMEOUT_SHORT); searchControl.setNoResultsString(google.search.SearchControl.NO_RESULTS_DEFAULT_STRING); // upon ajax search, refresh the url and search title searchControl.setSearchStartingCallback(this, function(control, searcher, query) { updateResultTitle(query); var query = document.getElementById('search_autocomplete').value; location.hash = 'q=' + query; }); // once search results load, set up click listeners searchControl.setSearchCompleteCallback(this, function(control, searcher, query) { addResultClickListeners(); }); // draw the search results box searchControl.draw(document.getElementById("leftSearchControl"), drawOptions); // get query and execute the search searchControl.execute(decodeURI(getQuery(location.hash))); document.getElementById("search_autocomplete").focus(); addTabListeners(); } // End of loadSearchResults google.setOnLoadCallback(function(){ if (location.hash.indexOf("q=") == -1) { // if there's no query in the url, don't search and make sure results are hidden $('#searchResults').hide(); return; } else { // first time loading search results for this page $('#searchResults').slideDown('slow', setStickyTop); $(".search .close").removeClass("hide"); loadSearchResults(); } }, true); /* Adjust the scroll position to account for sticky header, only if the hash matches an id. This does not handle <a name=""> tags. Some CSS fixes those, but only for reference docs. */ function offsetScrollForSticky() { // Ignore if there's no search bar (some special pages have no header) if ($("#search-container").length < 1) return; var hash = escape(location.hash.substr(1)); var $matchingElement = $("#"+hash); // Sanity check that there's an element with that ID on the page if ($matchingElement.length) { // If the position of the target element is near the top of the page (<20px, where we expect it // to be because we need to move it down 60px to become in view), then move it down 60px if (Math.abs($matchingElement.offset().top - $(window).scrollTop()) < 20) { $(window).scrollTop($(window).scrollTop() - 60); } } } // when an event on the browser history occurs (back, forward, load) requery hash and do search $(window).hashchange( function(){ // Ignore if there's no search bar (some special pages have no header) if ($("#search-container").length < 1) return; // If the hash isn't a search query or there's an error in the query, // then adjust the scroll position to account for sticky header, then exit. if ((location.hash.indexOf("q=") == -1) || (query == "undefined")) { // If the results pane is open, close it. if (!$("#searchResults").is(":hidden")) { hideResults(); } offsetScrollForSticky(); return; } // Otherwise, we have a search to do var query = decodeURI(getQuery(location.hash)); searchControl.execute(query); $('#searchResults').slideDown('slow', setStickyTop); $("#search_autocomplete").focus(); $(".search .close").removeClass("hide"); updateResultTitle(query); }); function updateResultTitle(query) { $("#searchTitle").html("Results for <em>" + escapeHTML(query) + "</em>"); } // forcefully regain key-up event control (previously jacked by search api) $("#search_autocomplete").keyup(function(event) { return search_changed(event, false, toRoot); }); // add event listeners to each tab so we can track the browser history function addTabListeners() { var tabHeaders = $(".gsc-tabHeader"); for (var i = 0; i < tabHeaders.length; i++) { $(tabHeaders[i]).attr("id",i).click(function() { /* // make a copy of the page numbers for the search left pane setTimeout(function() { // remove any residual page numbers $('#searchResults .gsc-tabsArea .gsc-cursor-box.gs-bidi-start-align').remove(); // move the page numbers to the left position; make a clone, // because the element is drawn to the DOM only once // and because we're going to remove it (previous line), // we need it to be available to move again as the user navigates $('#searchResults .gsc-webResult .gsc-cursor-box.gs-bidi-start-align:visible') .clone().appendTo('#searchResults .gsc-tabsArea'); }, 200); */ }); } setTimeout(function(){$(tabHeaders[0]).click()},200); } // add analytics tracking events to each result link function addResultClickListeners() { $("#searchResults a.gs-title").each(function(index, link) { // When user clicks enter for Google search results, track it $(link).click(function() { ga('send', 'event', 'Google Click', 'clicked: ' + $(this).attr('href'), 'query: ' + $("#search_autocomplete").val().toLowerCase()); }); }); } function getQuery(hash) { var queryParts = hash.split('='); return queryParts[1]; } /* returns the given string with all HTML brackets converted to entities TODO: move this to the site's JS library */ function escapeHTML(string) { return string.replace(/</g,"<") .replace(/>/g,">"); } /* ######################################################## */ /* ################# JAVADOC REFERENCE ################### */ /* ######################################################## */ /* Initialize some droiddoc stuff, but only if we're in the reference */ if (location.pathname.indexOf("/reference") == 0) { if(!(location.pathname.indexOf("/reference-gms/packages.html") == 0) && !(location.pathname.indexOf("/reference-gcm/packages.html") == 0) && !(location.pathname.indexOf("/reference/com/google") == 0)) { $(document).ready(function() { // init available apis based on user pref changeApiLevel(); initSidenavHeightResize() }); } } var API_LEVEL_COOKIE = "api_level"; var minLevel = 1; var maxLevel = 1; /******* SIDENAV DIMENSIONS ************/ function initSidenavHeightResize() { // Change the drag bar size to nicely fit the scrollbar positions var $dragBar = $(".ui-resizable-s"); $dragBar.css({'width': $dragBar.parent().width() - 5 + "px"}); $( "#resize-packages-nav" ).resizable({ containment: "#nav-panels", handles: "s", alsoResize: "#packages-nav", resize: function(event, ui) { resizeNav(); }, /* resize the nav while dragging */ stop: function(event, ui) { saveNavPanels(); } /* once stopped, save the sizes to cookie */ }); } function updateSidenavFixedWidth() { if (!sticky) return; $('#devdoc-nav').css({ 'width' : $('#side-nav').css('width'), 'margin' : $('#side-nav').css('margin') }); $('#devdoc-nav a.totop').css({'display':'block','width':$("#nav").innerWidth()+'px'}); initSidenavHeightResize(); } function updateSidenavFullscreenWidth() { if (!sticky) return; $('#devdoc-nav').css({ 'width' : $('#side-nav').css('width'), 'margin' : $('#side-nav').css('margin') }); $('#devdoc-nav .totop').css({'left': 'inherit'}); initSidenavHeightResize(); } function buildApiLevelSelector() { maxLevel = SINCE_DATA.length; var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE)); userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default minLevel = parseInt($("#doc-api-level").attr("class")); // Handle provisional api levels; the provisional level will always be the highest possible level // Provisional api levels will also have a length; other stuff that's just missing a level won't, // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class) if (isNaN(minLevel) && minLevel.length) { minLevel = maxLevel; } var select = $("#apiLevelSelector").html("").change(changeApiLevel); for (var i = maxLevel-1; i >= 0; i--) { var option = $("<option />").attr("value",""+SINCE_DATA[i]).append(""+SINCE_DATA[i]); // if (SINCE_DATA[i] < minLevel) option.addClass("absent"); // always false for strings (codenames) select.append(option); } // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true) var selectedLevelItem = $("#apiLevelSelector option[value='"+userApiLevel+"']").get(0); selectedLevelItem.setAttribute('selected',true); } function changeApiLevel() { maxLevel = SINCE_DATA.length; var selectedLevel = maxLevel; selectedLevel = parseInt($("#apiLevelSelector option:selected").val()); toggleVisisbleApis(selectedLevel, "body"); writeCookie(API_LEVEL_COOKIE, selectedLevel, null); if (selectedLevel < minLevel) { var thing = ($("#jd-header").html().indexOf("package") != -1) ? "package" : "class"; $("#naMessage").show().html("<div><p><strong>This " + thing + " requires API level " + minLevel + " or higher.</strong></p>" + "<p>This document is hidden because your selected API level for the documentation is " + selectedLevel + ". You can change the documentation API level with the selector " + "above the left navigation.</p>" + "<p>For more information about specifying the API level your app requires, " + "read <a href='" + toRoot + "training/basics/supporting-devices/platforms.html'" + ">Supporting Different Platform Versions</a>.</p>" + "<input type='button' value='OK, make this page visible' " + "title='Change the API level to " + minLevel + "' " + "onclick='$(\"#apiLevelSelector\").val(\"" + minLevel + "\");changeApiLevel();' />" + "</div>"); } else { $("#naMessage").hide(); } } function toggleVisisbleApis(selectedLevel, context) { var apis = $(".api",context); apis.each(function(i) { var obj = $(this); var className = obj.attr("class"); var apiLevelIndex = className.lastIndexOf("-")+1; var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex); apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length; var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex); if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail return; } apiLevel = parseInt(apiLevel); // Handle provisional api levels; if this item's level is the provisional one, set it to the max var selectedLevelNum = parseInt(selectedLevel) var apiLevelNum = parseInt(apiLevel); if (isNaN(apiLevelNum)) { apiLevelNum = maxLevel; } // Grey things out that aren't available and give a tooltip title if (apiLevelNum > selectedLevelNum) { obj.addClass("absent").attr("title","Requires API Level \"" + apiLevel + "\" or higher. To reveal, change the target API level " + "above the left navigation."); } else obj.removeClass("absent").removeAttr("title"); }); } /* ################# SIDENAV TREE VIEW ################### */ function new_node(me, mom, text, link, children_data, api_level) { var node = new Object(); node.children = Array(); node.children_data = children_data; node.depth = mom.depth + 1; node.li = document.createElement("li"); mom.get_children_ul().appendChild(node.li); node.label_div = document.createElement("div"); node.label_div.className = "label"; if (api_level != null) { $(node.label_div).addClass("api"); $(node.label_div).addClass("api-level-"+api_level); } node.li.appendChild(node.label_div); if (children_data != null) { node.expand_toggle = document.createElement("a"); node.expand_toggle.href = "javascript:void(0)"; node.expand_toggle.onclick = function() { if (node.expanded) { $(node.get_children_ul()).slideUp("fast"); node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png"; node.expanded = false; } else { expand_node(me, node); } }; node.label_div.appendChild(node.expand_toggle); node.plus_img = document.createElement("img"); node.plus_img.src = me.toroot + "assets/images/triangle-closed-small.png"; node.plus_img.className = "plus"; node.plus_img.width = "8"; node.plus_img.border = "0"; node.expand_toggle.appendChild(node.plus_img); node.expanded = false; } var a = document.createElement("a"); node.label_div.appendChild(a); node.label = document.createTextNode(text); a.appendChild(node.label); if (link) { a.href = me.toroot + link; } else { if (children_data != null) { a.className = "nolink"; a.href = "javascript:void(0)"; a.onclick = node.expand_toggle.onclick; // This next line shouldn't be necessary. I'll buy a beer for the first // person who figures out how to remove this line and have the link // toggle shut on the first try. --joeo@android.com node.expanded = false; } } node.children_ul = null; node.get_children_ul = function() { if (!node.children_ul) { node.children_ul = document.createElement("ul"); node.children_ul.className = "children_ul"; node.children_ul.style.display = "none"; node.li.appendChild(node.children_ul); } return node.children_ul; }; return node; } function expand_node(me, node) { if (node.children_data && !node.expanded) { if (node.children_visited) { $(node.get_children_ul()).slideDown("fast"); } else { get_node(me, node); if ($(node.label_div).hasClass("absent")) { $(node.get_children_ul()).addClass("absent"); } $(node.get_children_ul()).slideDown("fast"); } node.plus_img.src = me.toroot + "assets/images/triangle-opened-small.png"; node.expanded = true; // perform api level toggling because new nodes are new to the DOM var selectedLevel = $("#apiLevelSelector option:selected").val(); toggleVisisbleApis(selectedLevel, "#side-nav"); } } function get_node(me, mom) { mom.children_visited = true; for (var i in mom.children_data) { var node_data = mom.children_data[i]; mom.children[i] = new_node(me, mom, node_data[0], node_data[1], node_data[2], node_data[3]); } } function this_page_relative(toroot) { var full = document.location.pathname; var file = ""; if (toroot.substr(0, 1) == "/") { if (full.substr(0, toroot.length) == toroot) { return full.substr(toroot.length); } else { // the file isn't under toroot. Fail. return null; } } else { if (toroot != "./") { toroot = "./" + toroot; } do { if (toroot.substr(toroot.length-3, 3) == "../" || toroot == "./") { var pos = full.lastIndexOf("/"); file = full.substr(pos) + file; full = full.substr(0, pos); toroot = toroot.substr(0, toroot.length-3); } } while (toroot != "" && toroot != "/"); return file.substr(1); } } function find_page(url, data) { var nodes = data; var result = null; for (var i in nodes) { var d = nodes[i]; if (d[1] == url) { return new Array(i); } else if (d[2] != null) { result = find_page(url, d[2]); if (result != null) { return (new Array(i).concat(result)); } } } return null; } function init_default_navtree(toroot) { // load json file for navtree data $.getScript(toRoot + 'navtree_data.js', function(data, textStatus, jqxhr) { // when the file is loaded, initialize the tree if(jqxhr.status === 200) { init_navtree("tree-list", toroot, NAVTREE_DATA); } }); // perform api level toggling because because the whole tree is new to the DOM var selectedLevel = $("#apiLevelSelector option:selected").val(); toggleVisisbleApis(selectedLevel, "#side-nav"); } function init_navtree(navtree_id, toroot, root_nodes) { var me = new Object(); me.toroot = toroot; me.node = new Object(); me.node.li = document.getElementById(navtree_id); me.node.children_data = root_nodes; me.node.children = new Array(); me.node.children_ul = document.createElement("ul"); me.node.get_children_ul = function() { return me.node.children_ul; }; //me.node.children_ul.className = "children_ul"; me.node.li.appendChild(me.node.children_ul); me.node.depth = 0; get_node(me, me.node); me.this_page = this_page_relative(toroot); me.breadcrumbs = find_page(me.this_page, root_nodes); if (me.breadcrumbs != null && me.breadcrumbs.length != 0) { var mom = me.node; for (var i in me.breadcrumbs) { var j = me.breadcrumbs[i]; mom = mom.children[j]; expand_node(me, mom); } mom.label_div.className = mom.label_div.className + " selected"; addLoadEvent(function() { scrollIntoView("nav-tree"); }); } } /* TODO: eliminate redundancy with non-google functions */ function init_google_navtree(navtree_id, toroot, root_nodes) { var me = new Object(); me.toroot = toroot; me.node = new Object(); me.node.li = document.getElementById(navtree_id); me.node.children_data = root_nodes; me.node.children = new Array(); me.node.children_ul = document.createElement("ul"); me.node.get_children_ul = function() { return me.node.children_ul; }; //me.node.children_ul.className = "children_ul"; me.node.li.appendChild(me.node.children_ul); me.node.depth = 0; get_google_node(me, me.node); } function new_google_node(me, mom, text, link, children_data, api_level) { var node = new Object(); var child; node.children = Array(); node.children_data = children_data; node.depth = mom.depth + 1; node.get_children_ul = function() { if (!node.children_ul) { node.children_ul = document.createElement("ul"); node.children_ul.className = "tree-list-children"; node.li.appendChild(node.children_ul); } return node.children_ul; }; node.li = document.createElement("li"); mom.get_children_ul().appendChild(node.li); if(link) { child = document.createElement("a"); } else { child = document.createElement("span"); child.className = "tree-list-subtitle"; } if (children_data != null) { node.li.className="nav-section"; node.label_div = document.createElement("div"); node.label_div.className = "nav-section-header-ref"; node.li.appendChild(node.label_div); get_google_node(me, node); node.label_div.appendChild(child); } else { node.li.appendChild(child); } if(link) { child.href = me.toroot + link; } node.label = document.createTextNode(text); child.appendChild(node.label); node.children_ul = null; return node; } function get_google_node(me, mom) { mom.children_visited = true; var linkText; for (var i in mom.children_data) { var node_data = mom.children_data[i]; linkText = node_data[0]; if(linkText.match("^"+"com.google.android")=="com.google.android"){ linkText = linkText.substr(19, linkText.length); } mom.children[i] = new_google_node(me, mom, linkText, node_data[1], node_data[2], node_data[3]); } } /****** NEW version of script to build google and sample navs dynamically ******/ // TODO: update Google reference docs to tolerate this new implementation var NODE_NAME = 0; var NODE_HREF = 1; var NODE_GROUP = 2; var NODE_TAGS = 3; var NODE_CHILDREN = 4; function init_google_navtree2(navtree_id, data) { var $containerUl = $("#"+navtree_id); for (var i in data) { var node_data = data[i]; $containerUl.append(new_google_node2(node_data)); } // Make all third-generation list items 'sticky' to prevent them from collapsing $containerUl.find('li li li.nav-section').addClass('sticky'); initExpandableNavItems("#"+navtree_id); } function new_google_node2(node_data) { var linkText = node_data[NODE_NAME]; if(linkText.match("^"+"com.google.android")=="com.google.android"){ linkText = linkText.substr(19, linkText.length); } var $li = $('<li>'); var $a; if (node_data[NODE_HREF] != null) { $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >' + linkText + '</a>'); } else { $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >' + linkText + '/</a>'); } var $childUl = $('<ul>'); if (node_data[NODE_CHILDREN] != null) { $li.addClass("nav-section"); $a = $('<div class="nav-section-header">').append($a); if (node_data[NODE_HREF] == null) $a.addClass('empty'); for (var i in node_data[NODE_CHILDREN]) { var child_node_data = node_data[NODE_CHILDREN][i]; $childUl.append(new_google_node2(child_node_data)); } $li.append($childUl); } $li.prepend($a); return $li; } function showGoogleRefTree() { init_default_google_navtree(toRoot); init_default_gcm_navtree(toRoot); } function init_default_google_navtree(toroot) { // load json file for navtree data $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) { // when the file is loaded, initialize the tree if(jqxhr.status === 200) { init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA); highlightSidenav(); resizeNav(); } }); } function init_default_gcm_navtree(toroot) { // load json file for navtree data $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) { // when the file is loaded, initialize the tree if(jqxhr.status === 200) { init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA); highlightSidenav(); resizeNav(); } }); } function showSamplesRefTree() { init_default_samples_navtree(toRoot); } function init_default_samples_navtree(toroot) { // load json file for navtree data $.getScript(toRoot + 'samples_navtree_data.js', function(data, textStatus, jqxhr) { // when the file is loaded, initialize the tree if(jqxhr.status === 200) { // hack to remove the "about the samples" link then put it back in // after we nuke the list to remove the dummy static list of samples var $firstLi = $("#nav.samples-nav > li:first-child").clone(); $("#nav.samples-nav").empty(); $("#nav.samples-nav").append($firstLi); init_google_navtree2("nav.samples-nav", SAMPLES_NAVTREE_DATA); highlightSidenav(); resizeNav(); if ($("#jd-content #samples").length) { showSamples(); } } }); } /* TOGGLE INHERITED MEMBERS */ /* Toggle an inherited class (arrow toggle) * @param linkObj The link that was clicked. * @param expand 'true' to ensure it's expanded. 'false' to ensure it's closed. * 'null' to simply toggle. */ function toggleInherited(linkObj, expand) { var base = linkObj.getAttribute("id"); var list = document.getElementById(base + "-list"); var summary = document.getElementById(base + "-summary"); var trigger = document.getElementById(base + "-trigger"); var a = $(linkObj); if ( (expand == null && a.hasClass("closed")) || expand ) { list.style.display = "none"; summary.style.display = "block"; trigger.src = toRoot + "assets/images/triangle-opened.png"; a.removeClass("closed"); a.addClass("opened"); } else if ( (expand == null && a.hasClass("opened")) || (expand == false) ) { list.style.display = "block"; summary.style.display = "none"; trigger.src = toRoot + "assets/images/triangle-closed.png"; a.removeClass("opened"); a.addClass("closed"); } return false; } /* Toggle all inherited classes in a single table (e.g. all inherited methods) * @param linkObj The link that was clicked. * @param expand 'true' to ensure it's expanded. 'false' to ensure it's closed. * 'null' to simply toggle. */ function toggleAllInherited(linkObj, expand) { var a = $(linkObj); var table = $(a.parent().parent().parent()); // ugly way to get table/tbody var expandos = $(".jd-expando-trigger", table); if ( (expand == null && a.text() == "[Expand]") || expand ) { expandos.each(function(i) { toggleInherited(this, true); }); a.text("[Collapse]"); } else if ( (expand == null && a.text() == "[Collapse]") || (expand == false) ) { expandos.each(function(i) { toggleInherited(this, false); }); a.text("[Expand]"); } return false; } /* Toggle all inherited members in the class (link in the class title) */ function toggleAllClassInherited() { var a = $("#toggleAllClassInherited"); // get toggle link from class title var toggles = $(".toggle-all", $("#body-content")); if (a.text() == "[Expand All]") { toggles.each(function(i) { toggleAllInherited(this, true); }); a.text("[Collapse All]"); } else { toggles.each(function(i) { toggleAllInherited(this, false); }); a.text("[Expand All]"); } return false; } /* Expand all inherited members in the class. Used when initiating page search */ function ensureAllInheritedExpanded() { var toggles = $(".toggle-all", $("#body-content")); toggles.each(function(i) { toggleAllInherited(this, true); }); $("#toggleAllClassInherited").text("[Collapse All]"); } /* HANDLE KEY EVENTS * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search) */ var agent = navigator['userAgent'].toLowerCase(); var mac = agent.indexOf("macintosh") != -1; $(document).keydown( function(e) { var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key if (control && e.which == 70) { // 70 is "F" ensureAllInheritedExpanded(); } }); /* On-demand functions */ /** Move sample code line numbers out of PRE block and into non-copyable column */ function initCodeLineNumbers() { var numbers = $("#codesample-block a.number"); if (numbers.length) { $("#codesample-line-numbers").removeClass("hidden").append(numbers); } $(document).ready(function() { // select entire line when clicked $("span.code-line").click(function() { if (!shifted) { selectText(this); } }); // invoke line link on double click $(".code-line").dblclick(function() { document.location.hash = $(this).attr('id'); }); // highlight the line when hovering on the number $("#codesample-line-numbers a.number").mouseover(function() { var id = $(this).attr('href'); $(id).css('background','#e7e7e7'); }); $("#codesample-line-numbers a.number").mouseout(function() { var id = $(this).attr('href'); $(id).css('background','none'); }); }); } // create SHIFT key binder to avoid the selectText method when selecting multiple lines var shifted = false; $(document).bind('keyup keydown', function(e){shifted = e.shiftKey; return true;} ); // courtesy of jasonedelman.com function selectText(element) { var doc = document , range, selection ; if (doc.body.createTextRange) { //ms range = doc.body.createTextRange(); range.moveToElementText(element); range.select(); } else if (window.getSelection) { //all others selection = window.getSelection(); range = doc.createRange(); range.selectNodeContents(element); selection.removeAllRanges(); selection.addRange(range); } } /** Display links and other information about samples that match the group specified by the URL */ function showSamples() { var group = $("#samples").attr('class'); $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>"); var $ul = $("<ul>"); $selectedLi = $("#nav li.selected"); $selectedLi.children("ul").children("li").each(function() { var $li = $("<li>").append($(this).find("a").first().clone()); $ul.append($li); }); $("#samples").append($ul); } /* ########################################################## */ /* ################### RESOURCE CARDS ##################### */ /* ########################################################## */ /** Handle resource queries, collections, and grids (sections). Requires jd_tag_helpers.js and the *_unified_data.js to be loaded. */ (function() { // Prevent the same resource from being loaded more than once per page. var addedPageResources = {}; $(document).ready(function() { $('.resource-widget').each(function() { initResourceWidget(this); }); /* Pass the line height to ellipsisfade() to adjust the height of the text container to show the max number of lines possible, without showing lines that are cut off. This works with the css ellipsis classes to fade last text line and apply an ellipsis char. */ //card text currently uses 15px line height. var lineHeight = 15; $('.card-info .text').ellipsisfade(lineHeight); }); /* Three types of resource layouts: Flow - Uses a fixed row-height flow using float left style. Carousel - Single card slideshow all same dimension absolute. Stack - Uses fixed columns and flexible element height. */ function initResourceWidget(widget) { var $widget = $(widget); var isFlow = $widget.hasClass('resource-flow-layout'), isCarousel = $widget.hasClass('resource-carousel-layout'), isStack = $widget.hasClass('resource-stack-layout'); // find size of widget by pulling out its class name var sizeCols = 1; var m = $widget.get(0).className.match(/\bcol-(\d+)\b/); if (m) { sizeCols = parseInt(m[1], 10); } var opts = { cardSizes: ($widget.data('cardsizes') || '').split(','), maxResults: parseInt($widget.data('maxresults') || '100', 10), itemsPerPage: $widget.data('itemsperpage'), sortOrder: $widget.data('sortorder'), query: $widget.data('query'), section: $widget.data('section'), sizeCols: sizeCols, /* Added by LFL 6/6/14 */ resourceStyle: $widget.data('resourcestyle') || 'card', stackSort: $widget.data('stacksort') || 'true' }; // run the search for the set of resources to show var resources = buildResourceList(opts); if (isFlow) { drawResourcesFlowWidget($widget, opts, resources); } else if (isCarousel) { drawResourcesCarouselWidget($widget, opts, resources); } else if (isStack) { /* Looks like this got removed and is not used, so repurposing for the homepage style layout. Modified by LFL 6/6/14 */ //var sections = buildSectionList(opts); opts['numStacks'] = $widget.data('numstacks'); drawResourcesStackWidget($widget, opts, resources/*, sections*/); } } /* Initializes a Resource Carousel Widget */ function drawResourcesCarouselWidget($widget, opts, resources) { $widget.empty(); var plusone = true; //always show plusone on carousel $widget.addClass('resource-card slideshow-container') .append($('<a>').addClass('slideshow-prev').text('Prev')) .append($('<a>').addClass('slideshow-next').text('Next')); var css = { 'width': $widget.width() + 'px', 'height': $widget.height() + 'px' }; var $ul = $('<ul>'); for (var i = 0; i < resources.length; ++i) { var $card = $('<a>') .attr('href', cleanUrl(resources[i].url)) .decorateResourceCard(resources[i],plusone); $('<li>').css(css) .append($card) .appendTo($ul); } $('<div>').addClass('frame') .append($ul) .appendTo($widget); $widget.dacSlideshow({ auto: true, btnPrev: '.slideshow-prev', btnNext: '.slideshow-next' }); }; /* Initializes a Resource Card Stack Widget (column-based layout) Modified by LFL 6/6/14 */ function drawResourcesStackWidget($widget, opts, resources, sections) { // Don't empty widget, grab all items inside since they will be the first // items stacked, followed by the resource query var plusone = true; //by default show plusone on section cards var cards = $widget.find('.resource-card').detach().toArray(); var numStacks = opts.numStacks || 1; var $stacks = []; var urlString; for (var i = 0; i < numStacks; ++i) { $stacks[i] = $('<div>').addClass('resource-card-stack') .appendTo($widget); } var sectionResources = []; // Extract any subsections that are actually resource cards if (sections) { for (var i = 0; i < sections.length; ++i) { if (!sections[i].sections || !sections[i].sections.length) { // Render it as a resource card sectionResources.push( $('<a>') .addClass('resource-card section-card') .attr('href', cleanUrl(sections[i].resource.url)) .decorateResourceCard(sections[i].resource,plusone)[0] ); } else { cards.push( $('<div>') .addClass('resource-card section-card-menu') .decorateResourceSection(sections[i],plusone)[0] ); } } } cards = cards.concat(sectionResources); for (var i = 0; i < resources.length; ++i) { var $card = createResourceElement(resources[i], opts); if (opts.resourceStyle.indexOf('related') > -1) { $card.addClass('related-card'); } cards.push($card[0]); } if (opts.stackSort != 'false') { for (var i = 0; i < cards.length; ++i) { // Find the stack with the shortest height, but give preference to // left to right order. var minHeight = $stacks[0].height(); var minIndex = 0; for (var j = 1; j < numStacks; ++j) { var height = $stacks[j].height(); if (height < minHeight - 45) { minHeight = height; minIndex = j; } } $stacks[minIndex].append($(cards[i])); } } }; /* Create a resource card using the given resource object and a list of html configured options. Returns a jquery object containing the element. */ function createResourceElement(resource, opts, plusone) { var $el; // The difference here is that generic cards are not entirely clickable // so its a div instead of an a tag, also the generic one is not given // the resource-card class so it appears with a transparent background // and can be styled in whatever way the css setup. if (opts.resourceStyle == 'generic') { $el = $('<div>') .addClass('resource') .attr('href', cleanUrl(resource.url)) .decorateResource(resource, opts); } else { var cls = 'resource resource-card'; $el = $('<a>') .addClass(cls) .attr('href', cleanUrl(resource.url)) .decorateResourceCard(resource, plusone); } return $el; } /* Initializes a flow widget, see distribute.scss for generating accompanying css */ function drawResourcesFlowWidget($widget, opts, resources) { $widget.empty(); var cardSizes = opts.cardSizes || ['6x6']; var i = 0, j = 0; var plusone = true; // by default show plusone on resource cards while (i < resources.length) { var cardSize = cardSizes[j++ % cardSizes.length]; cardSize = cardSize.replace(/^\s+|\s+$/,''); // Some card sizes do not get a plusone button, such as where space is constrained // or for cards commonly embedded in docs (to improve overall page speed). plusone = !((cardSize == "6x2") || (cardSize == "6x3") || (cardSize == "9x2") || (cardSize == "9x3") || (cardSize == "12x2") || (cardSize == "12x3")); // A stack has a third dimension which is the number of stacked items var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/); var stackCount = 0; var $stackDiv = null; if (isStack) { // Create a stack container which should have the dimensions defined // by the product of the items inside. $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1] + 'x' + isStack[2] * isStack[3]) .appendTo($widget); } // Build each stack item or just a single item do { var resource = resources[i]; var $card = createResourceElement(resources[i], opts, plusone); $card.addClass('resource-card-' + cardSize + ' resource-card-' + resource.type); if (isStack) { $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]); if (++stackCount == parseInt(isStack[3])) { $card.addClass('resource-card-row-stack-last'); stackCount = 0; } } else { stackCount = 0; } $card.appendTo($stackDiv || $widget); } while (++i < resources.length && stackCount > 0); } } /* Build a site map of resources using a section as a root. */ function buildSectionList(opts) { if (opts.section && SECTION_BY_ID[opts.section]) { return SECTION_BY_ID[opts.section].sections || []; } return []; } function buildResourceList(opts) { var maxResults = opts.maxResults || 100; var query = opts.query || ''; var expressions = parseResourceQuery(query); var addedResourceIndices = {}; var results = []; for (var i = 0; i < expressions.length; i++) { var clauses = expressions[i]; // build initial set of resources from first clause var firstClause = clauses[0]; var resources = []; switch (firstClause.attr) { case 'type': resources = ALL_RESOURCES_BY_TYPE[firstClause.value]; break; case 'lang': resources = ALL_RESOURCES_BY_LANG[firstClause.value]; break; case 'tag': resources = ALL_RESOURCES_BY_TAG[firstClause.value]; break; case 'collection': var urls = RESOURCE_COLLECTIONS[firstClause.value].resources || []; resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; }); break; case 'section': var urls = SITE_MAP[firstClause.value].sections || []; resources = urls.map(function(url){ return ALL_RESOURCES_BY_URL[url]; }); break; } // console.log(firstClause.attr + ':' + firstClause.value); resources = resources || []; // use additional clauses to filter corpus if (clauses.length > 1) { var otherClauses = clauses.slice(1); resources = resources.filter(getResourceMatchesClausesFilter(otherClauses)); } // filter out resources already added if (i > 1) { resources = resources.filter(getResourceNotAlreadyAddedFilter(addedResourceIndices)); } // add to list of already added indices for (var j = 0; j < resources.length; j++) { // console.log(resources[j].title); addedResourceIndices[resources[j].index] = 1; } // concat to final results list results = results.concat(resources); } if (opts.sortOrder && results.length) { var attr = opts.sortOrder; if (opts.sortOrder == 'random') { var i = results.length, j, temp; while (--i) { j = Math.floor(Math.random() * (i + 1)); temp = results[i]; results[i] = results[j]; results[j] = temp; } } else { var desc = attr.charAt(0) == '-'; if (desc) { attr = attr.substring(1); } results = results.sort(function(x,y) { return (desc ? -1 : 1) * (parseInt(x[attr], 10) - parseInt(y[attr], 10)); }); } } results = results.filter(getResourceNotAlreadyAddedFilter(addedPageResources)); results = results.slice(0, maxResults); for (var j = 0; j < results.length; ++j) { addedPageResources[results[j].index] = 1; } return results; } function getResourceNotAlreadyAddedFilter(addedResourceIndices) { return function(resource) { return !addedResourceIndices[resource.index]; }; } function getResourceMatchesClausesFilter(clauses) { return function(resource) { return doesResourceMatchClauses(resource, clauses); }; } function doesResourceMatchClauses(resource, clauses) { for (var i = 0; i < clauses.length; i++) { var map; switch (clauses[i].attr) { case 'type': map = IS_RESOURCE_OF_TYPE[clauses[i].value]; break; case 'lang': map = IS_RESOURCE_IN_LANG[clauses[i].value]; break; case 'tag': map = IS_RESOURCE_TAGGED[clauses[i].value]; break; } if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) { return clauses[i].negative; } } return true; } function cleanUrl(url) { if (url && url.indexOf('//') === -1) { url = toRoot + url; } return url; } function parseResourceQuery(query) { // Parse query into array of expressions (expression e.g. 'tag:foo + type:video') var expressions = []; var expressionStrs = query.split(',') || []; for (var i = 0; i < expressionStrs.length; i++) { var expr = expressionStrs[i] || ''; // Break expression into clauses (clause e.g. 'tag:foo') var clauses = []; var clauseStrs = expr.split(/(?=[\+\-])/); for (var j = 0; j < clauseStrs.length; j++) { var clauseStr = clauseStrs[j] || ''; // Get attribute and value from clause (e.g. attribute='tag', value='foo') var parts = clauseStr.split(':'); var clause = {}; clause.attr = parts[0].replace(/^\s+|\s+$/g,''); if (clause.attr) { if (clause.attr.charAt(0) == '+') { clause.attr = clause.attr.substring(1); } else if (clause.attr.charAt(0) == '-') { clause.negative = true; clause.attr = clause.attr.substring(1); } } if (parts.length > 1) { clause.value = parts[1].replace(/^\s+|\s+$/g,''); } clauses.push(clause); } if (!clauses.length) { continue; } expressions.push(clauses); } return expressions; } })(); (function($) { /* Utility method for creating dom for the description area of a card. Used in decorateResourceCard and decorateResource. */ function buildResourceCardDescription(resource, plusone) { var $description = $('<div>').addClass('description ellipsis'); $description.append($('<div>').addClass('text').html(resource.summary)); if (resource.cta) { $description.append($('<a>').addClass('cta').html(resource.cta)); } if (plusone) { var plusurl = resource.url.indexOf("//") > -1 ? resource.url : "//developer.android.com/" + resource.url; $description.append($('<div>').addClass('util') .append($('<div>').addClass('g-plusone') .attr('data-size', 'small') .attr('data-align', 'right') .attr('data-href', plusurl))); } return $description; } /* Simple jquery function to create dom for a standard resource card */ $.fn.decorateResourceCard = function(resource,plusone) { var section = resource.group || resource.type; var imgUrl = resource.image || 'assets/images/resource-card-default-android.jpg'; if (imgUrl.indexOf('//') === -1) { imgUrl = toRoot + imgUrl; } $('<div>').addClass('card-bg') .css('background-image', 'url(' + (imgUrl || toRoot + 'assets/images/resource-card-default-android.jpg') + ')') .appendTo(this); $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : '')) .append($('<div>').addClass('section').text(section)) .append($('<div>').addClass('title').html(resource.title)) .append(buildResourceCardDescription(resource, plusone)) .appendTo(this); return this; }; /* Simple jquery function to create dom for a resource section card (menu) */ $.fn.decorateResourceSection = function(section,plusone) { var resource = section.resource; //keep url clean for matching and offline mode handling var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot; var $base = $('<a>') .addClass('card-bg') .attr('href', resource.url) .append($('<div>').addClass('card-section-icon') .append($('<div>').addClass('icon')) .append($('<div>').addClass('section').html(resource.title))) .appendTo(this); var $cardInfo = $('<div>').addClass('card-info').appendTo(this); if (section.sections && section.sections.length) { // Recurse the section sub-tree to find a resource image. var stack = [section]; while (stack.length) { if (stack[0].resource.image) { $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')'); break; } if (stack[0].sections) { stack = stack.concat(stack[0].sections); } stack.shift(); } var $ul = $('<ul>') .appendTo($cardInfo); var max = section.sections.length > 3 ? 3 : section.sections.length; for (var i = 0; i < max; ++i) { var subResource = section.sections[i]; if (!plusone) { $('<li>') .append($('<a>').attr('href', subResource.url) .append($('<div>').addClass('title').html(subResource.title)) .append($('<div>').addClass('description ellipsis') .append($('<div>').addClass('text').html(subResource.summary)) .append($('<div>').addClass('util')))) .appendTo($ul); } else { $('<li>') .append($('<a>').attr('href', subResource.url) .append($('<div>').addClass('title').html(subResource.title)) .append($('<div>').addClass('description ellipsis') .append($('<div>').addClass('text').html(subResource.summary)) .append($('<div>').addClass('util') .append($('<div>').addClass('g-plusone') .attr('data-size', 'small') .attr('data-align', 'right') .attr('data-href', resource.url))))) .appendTo($ul); } } // Add a more row if (max < section.sections.length) { $('<li>') .append($('<a>').attr('href', resource.url) .append($('<div>') .addClass('title') .text('More'))) .appendTo($ul); } } else { // No sub-resources, just render description? } return this; }; /* Render other types of resource styles that are not cards. */ $.fn.decorateResource = function(resource, opts) { var imgUrl = resource.image || 'assets/images/resource-card-default-android.jpg'; var linkUrl = resource.url; if (imgUrl.indexOf('//') === -1) { imgUrl = toRoot + imgUrl; } if (linkUrl && linkUrl.indexOf('//') === -1) { linkUrl = toRoot + linkUrl; } $(this).append( $('<div>').addClass('image') .css('background-image', 'url(' + imgUrl + ')'), $('<div>').addClass('info').append( $('<h4>').addClass('title').html(resource.title), $('<p>').addClass('summary').html(resource.summary), $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More') ) ); return this; }; })(jQuery); /* Calculate the vertical area remaining */ (function($) { $.fn.ellipsisfade= function(lineHeight) { this.each(function() { // get element text var $this = $(this); var remainingHeight = $this.parent().parent().height(); $this.parent().siblings().each(function () { if ($(this).is(":visible")) { var h = $(this).height(); remainingHeight = remainingHeight - h; } }); adjustedRemainingHeight = ((remainingHeight)/lineHeight>>0)*lineHeight $this.parent().css({'height': adjustedRemainingHeight}); $this.css({'height': "auto"}); }); return this; }; }) (jQuery); /* Fullscreen Carousel The following allows for an area at the top of the page that takes over the entire browser height except for its top offset and an optional bottom padding specified as a data attribute. HTML: <div class="fullscreen-carousel"> <div class="fullscreen-carousel-content"> <!-- content here --> </div> <div class="fullscreen-carousel-content"> <!-- content here --> </div> etc ... </div> Control over how the carousel takes over the screen can mostly be defined in a css file. Setting min-height on the .fullscreen-carousel-content elements will prevent them from shrinking to far vertically when the browser is very short, and setting max-height on the .fullscreen-carousel itself will prevent the area from becoming to long in the case that the browser is stretched very tall. There is limited functionality for having multiple sections since that request was removed, but it is possible to add .next-arrow and .prev-arrow elements to scroll between multiple content areas. */ (function() { $(document).ready(function() { $('.fullscreen-carousel').each(function() { initWidget(this); }); }); function initWidget(widget) { var $widget = $(widget); var topOffset = $widget.offset().top; var padBottom = parseInt($widget.data('paddingbottom')) || 0; var maxHeight = 0; var minHeight = 0; var $content = $widget.find('.fullscreen-carousel-content'); var $nextArrow = $widget.find('.next-arrow'); var $prevArrow = $widget.find('.prev-arrow'); var $curSection = $($content[0]); if ($content.length <= 1) { $nextArrow.hide(); $prevArrow.hide(); } else { $nextArrow.click(function() { var index = ($content.index($curSection) + 1); $curSection.hide(); $curSection = $($content[index >= $content.length ? 0 : index]); $curSection.show(); }); $prevArrow.click(function() { var index = ($content.index($curSection) - 1); $curSection.hide(); $curSection = $($content[index < 0 ? $content.length - 1 : 0]); $curSection.show(); }); } // Just hide all content sections except first. $content.each(function(index) { if ($(this).height() > minHeight) minHeight = $(this).height(); $(this).css({position: 'absolute', display: index > 0 ? 'none' : ''}); }); // Register for changes to window size, and trigger. $(window).resize(resizeWidget); resizeWidget(); function resizeWidget() { var height = $(window).height() - topOffset - padBottom; $widget.width($(window).width()); $widget.height(height < minHeight ? minHeight : (maxHeight && height > maxHeight ? maxHeight : height)); } } })(); /* Tab Carousel The following allows tab widgets to be installed via the html below. Each tab content section should have a data-tab attribute matching one of the nav items'. Also each tab content section should have a width matching the tab carousel. HTML: <div class="tab-carousel"> <ul class="tab-nav"> <li><a href="#" data-tab="handsets">Handsets</a> <li><a href="#" data-tab="wearable">Wearable</a> <li><a href="#" data-tab="tv">TV</a> </ul> <div class="tab-carousel-content"> <div data-tab="handsets"> <!--Full width content here--> </div> <div data-tab="wearable"> <!--Full width content here--> </div> <div data-tab="tv"> <!--Full width content here--> </div> </div> </div> */ (function() { $(document).ready(function() { $('.tab-carousel').each(function() { initWidget(this); }); }); function initWidget(widget) { var $widget = $(widget); var $nav = $widget.find('.tab-nav'); var $anchors = $nav.find('[data-tab]'); var $li = $nav.find('li'); var $contentContainer = $widget.find('.tab-carousel-content'); var $tabs = $contentContainer.find('[data-tab]'); var $curTab = $($tabs[0]); // Current tab is first tab. var width = $widget.width(); // Setup nav interactivity. $anchors.click(function(evt) { evt.preventDefault(); var query = '[data-tab=' + $(this).data('tab') + ']'; transitionWidget($tabs.filter(query)); }); // Add highlight for navigation on first item. var $highlight = $('<div>').addClass('highlight') .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'}) .appendTo($nav); // Store height since we will change contents to absolute. $contentContainer.height($contentContainer.height()); // Absolutely position tabs so they're ready for transition. $tabs.each(function(index) { $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'}); }); function transitionWidget($toTab) { if (!$curTab.is($toTab)) { var curIndex = $tabs.index($curTab[0]); var toIndex = $tabs.index($toTab[0]); var dir = toIndex > curIndex ? 1 : -1; // Animate content sections. $toTab.css({left:(width * dir) + 'px'}); $curTab.animate({left:(width * -dir) + 'px'}); $toTab.animate({left:'0'}); // Animate navigation highlight. $highlight.animate({left:$($li[toIndex]).position().left + 'px', width:$($li[toIndex]).outerWidth() + 'px'}) // Store new current section. $curTab = $toTab; } } } })();