1var cookie_namespace = 'android_developer'; 2var isMobile = false; // true if mobile, so we can adjust some layout 3var mPagePath; // initialized in ready() function 4 5var basePath = getBaseUri(location.pathname); 6var SITE_ROOT = toRoot + basePath.substring(1, basePath.indexOf("/", 1)); 7 8// TODO(akassay) generate this var in the reference doc build. 9var API_LEVELS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 10 '10', '11', '12', '13', '14', '15', '16', 11 '17', '18', '19', '20', '21', '22', '23', '24', '25', 'O']; 12var METADATA = METADATA || {}; 13var RESERVED_METADATA_CATEGORY_NAMES = ['extras', 'carousel', 'collections', 14 'searchHeroCollections']; 15 16// Ensure that all ajax getScript() requests allow caching 17$.ajaxSetup({ 18 cache: true 19}); 20 21/****** ON LOAD SET UP STUFF *********/ 22 23$(document).ready(function() { 24 25 // prep nav expandos 26 var pagePath = location.href.replace(location.hash, ''); 27 // account for intl docs by removing the intl/*/ path 28 if (pagePath.indexOf("/intl/") == 0) { 29 pagePath = pagePath.substr(pagePath.indexOf("/", 6)); // start after intl/ to get last / 30 } 31 32 if (pagePath.indexOf(SITE_ROOT) == 0) { 33 if (pagePath == '' || pagePath.charAt(pagePath.length - 1) == '/') { 34 pagePath += 'index.html'; 35 } 36 } 37 38 // Need a copy of the pagePath before it gets changed in the next block; 39 // it's needed to perform proper tab highlighting in offline docs (see rootDir below) 40 var pagePathOriginal = pagePath; 41 if (SITE_ROOT.match(/\.\.\//) || SITE_ROOT == '') { 42 // If running locally, SITE_ROOT will be a relative path, so account for that by 43 // finding the relative URL to this page. This will allow us to find links on the page 44 // leading back to this page. 45 var pathParts = pagePath.split('/'); 46 var relativePagePathParts = []; 47 var upDirs = (SITE_ROOT.match(/(\.\.\/)+/) || [''])[0].length / 3; 48 for (var i = 0; i < upDirs; i++) { 49 relativePagePathParts.push('..'); 50 } 51 for (var i = 0; i < upDirs; i++) { 52 relativePagePathParts.push(pathParts[pathParts.length - (upDirs - i) - 1]); 53 } 54 relativePagePathParts.push(pathParts[pathParts.length - 1]); 55 pagePath = relativePagePathParts.join('/'); 56 } else { 57 // Otherwise the page path is already an absolute URL 58 } 59 60 // set global variable so we can highlight the sidenav a bit later (such as for google reference) 61 // and highlight the sidenav 62 mPagePath = pagePath; 63 64 // Check for params and remove them. 65 mPagePath = mPagePath.split('?')[0]; 66 highlightSidenav(); 67 68 // set up prev/next links if they exist 69 var $selNavLink = $('#nav').find('a[href="' + pagePath + '"]'); 70 var $selListItem; 71 if ($selNavLink.length) { 72 $selListItem = $selNavLink.closest('li'); 73 74 // set up prev links 75 var $prevLink = []; 76 var $prevListItem = $selListItem.prev('li'); 77 78 var crossBoundaries = ($("body.design").length > 0) || ($("body.guide").length > 0) ? true : 79false; // navigate across topic boundaries only in design docs 80 if ($prevListItem.length) { 81 if ($prevListItem.hasClass('nav-section') || crossBoundaries) { 82 // jump to last topic of previous section 83 $prevLink = $prevListItem.find('a:last'); 84 } else if (!$selListItem.hasClass('nav-section')) { 85 // jump to previous topic in this section 86 $prevLink = $prevListItem.find('a:eq(0)'); 87 } 88 } else { 89 // jump to this section's index page (if it exists) 90 var $parentListItem = $selListItem.parents('li'); 91 $prevLink = $selListItem.parents('li').find('a'); 92 93 // except if cross boundaries aren't allowed, and we're at the top of a section already 94 // (and there's another parent) 95 if (!crossBoundaries && $parentListItem.hasClass('nav-section') && 96 $selListItem.hasClass('nav-section')) { 97 $prevLink = []; 98 } 99 } 100 101 // set up next links 102 var $nextLink = []; 103 var startClass = false; 104 var isCrossingBoundary = false; 105 106 if ($selListItem.hasClass('nav-section') && $selListItem.children('div.empty').length == 0) { 107 // we're on an index page, jump to the first topic 108 $nextLink = $selListItem.find('ul:eq(0)').find('a:eq(0)'); 109 110 // if there aren't any children, go to the next section (required for About pages) 111 if ($nextLink.length == 0) { 112 $nextLink = $selListItem.next('li').find('a'); 113 } else if ($('.topic-start-link').length) { 114 // as long as there's a child link and there is a "topic start link" (we're on a landing) 115 // then set the landing page "start link" text to be the first doc title 116 $('.topic-start-link').text($nextLink.text().toUpperCase()); 117 } 118 119 // If the selected page has a description, then it's a class or article homepage 120 if ($selListItem.find('a[description]').length) { 121 // this means we're on a class landing page 122 startClass = true; 123 } 124 } else { 125 // jump to the next topic in this section (if it exists) 126 $nextLink = $selListItem.next('li').find('a:eq(0)'); 127 if ($nextLink.length == 0) { 128 isCrossingBoundary = true; 129 // no more topics in this section, jump to the first topic in the next section 130 $nextLink = $selListItem.parents('li:eq(0)').next('li').find('a:eq(0)'); 131 if (!$nextLink.length) { // Go up another layer to look for next page (lesson > class > course) 132 $nextLink = $selListItem.parents('li:eq(1)').next('li.nav-section').find('a:eq(0)'); 133 if ($nextLink.length == 0) { 134 // if that doesn't work, we're at the end of the list, so disable NEXT link 135 $('.next-page-link').attr('href', '').addClass("disabled") 136 .click(function() { return false; }); 137 // and completely hide the one in the footer 138 $('.content-footer .next-page-link').hide(); 139 } 140 } 141 } 142 } 143 144 if (startClass) { 145 $('.start-class-link').attr('href', $nextLink.attr('href')).removeClass("hide"); 146 147 // if there's no training bar (below the start button), 148 // then we need to add a bottom border to button 149 if (!$("#tb").length) { 150 $('.start-class-link').css({'border-bottom':'1px solid #DADADA'}); 151 } 152 } else if (isCrossingBoundary && !$('body.design').length) { // Design always crosses boundaries 153 $('.content-footer.next-class').show(); 154 $('.next-page-link').attr('href', '') 155 .removeClass("hide").addClass("disabled") 156 .click(function() { return false; }); 157 // and completely hide the one in the footer 158 $('.content-footer .next-page-link').hide(); 159 $('.content-footer .prev-page-link').hide(); 160 161 if ($nextLink.length) { 162 $('.next-class-link').attr('href', $nextLink.attr('href')) 163 .removeClass("hide"); 164 165 $('.content-footer .next-class-link').append($nextLink.html()); 166 167 $('.next-class-link').find('.new').empty(); 168 } 169 } else { 170 $('.next-page-link').attr('href', $nextLink.attr('href')) 171 .removeClass("hide"); 172 // for the footer link, also add the previous and next page titles 173 if ($prevLink.length) { 174 $('.content-footer .prev-page-link').append($prevLink.html()); 175 } 176 if ($nextLink.length) { 177 $('.content-footer .next-page-link').append($nextLink.html()); 178 } 179 } 180 181 if (!startClass && $prevLink.length) { 182 var prevHref = $prevLink.attr('href'); 183 if (prevHref == SITE_ROOT + 'index.html') { 184 // Don't show Previous when it leads to the homepage 185 } else { 186 $('.prev-page-link').attr('href', $prevLink.attr('href')).removeClass("hide"); 187 } 188 } 189 } 190 191 // Set up the course landing pages for Training with class names and descriptions 192 if ($('body.trainingcourse').length) { 193 var $classLinks = $selListItem.find('ul li a').not('#nav .nav-section .nav-section ul a'); 194 195 // create an array for all the class descriptions 196 var $classDescriptions = new Array($classLinks.length); 197 var lang = getLangPref(); 198 $classLinks.each(function(index) { 199 var langDescr = $(this).attr(lang + "-description"); 200 if (typeof langDescr !== 'undefined' && langDescr !== false) { 201 // if there's a class description in the selected language, use that 202 $classDescriptions[index] = langDescr; 203 } else { 204 // otherwise, use the default english description 205 $classDescriptions[index] = $(this).attr("description"); 206 } 207 }); 208 209 var $olClasses = $('<ol class="class-list"></ol>'); 210 var $liClass; 211 var $h2Title; 212 var $pSummary; 213 var $olLessons; 214 var $liLesson; 215 $classLinks.each(function(index) { 216 $liClass = $('<li class="clearfix"></li>'); 217 $h2Title = $('<a class="title" href="' + $(this).attr('href') + '"><h2 class="norule">' + $(this).html() + '</h2><span></span></a>'); 218 $pSummary = $('<p class="description">' + $classDescriptions[index] + '</p>'); 219 220 $olLessons = $('<ol class="lesson-list"></ol>'); 221 222 $lessons = $(this).closest('li').find('ul li a'); 223 224 if ($lessons.length) { 225 $lessons.each(function(index) { 226 $olLessons.append('<li><a href="' + $(this).attr('href') + '">' + $(this).html() + '</a></li>'); 227 }); 228 } else { 229 $pSummary.addClass('article'); 230 } 231 232 $liClass.append($h2Title).append($pSummary).append($olLessons); 233 $olClasses.append($liClass); 234 }); 235 $('#classes').append($olClasses); 236 } 237 238 // Set up expand/collapse behavior 239 initExpandableNavItems("#nav"); 240 241 // Set up play-on-hover <video> tags. 242 $('video.play-on-hover').bind('click', function() { 243 $(this).get(0).load(); // in case the video isn't seekable 244 $(this).get(0).play(); 245 }); 246 247 // Set up play-on-click for <video> tags with a "video-wrapper". 248 $('.video-wrapper > video').bind('click', function() { 249 this.play(); 250 $(this.parentElement).addClass('playing'); 251 }); 252 253 // Set up tooltips 254 var TOOLTIP_MARGIN = 10; 255 $('acronym,.tooltip-link').each(function() { 256 var $target = $(this); 257 var $tooltip = $('<div>') 258 .addClass('tooltip-box') 259 .append($target.attr('title')) 260 .hide() 261 .appendTo('body'); 262 $target.removeAttr('title'); 263 264 $target.hover(function() { 265 // in 266 var targetRect = $target.offset(); 267 targetRect.width = $target.width(); 268 targetRect.height = $target.height(); 269 270 $tooltip.css({ 271 left: targetRect.left, 272 top: targetRect.top + targetRect.height + TOOLTIP_MARGIN 273 }); 274 $tooltip.addClass('below'); 275 $tooltip.show(); 276 }, function() { 277 // out 278 $tooltip.hide(); 279 }); 280 }); 281 282 // Set up <h2> deeplinks 283 $('h2').click(function() { 284 var id = $(this).attr('id'); 285 if (id) { 286 if (history && history.replaceState) { 287 // Change url without scrolling. 288 history.replaceState({}, '', '#' + id); 289 } else { 290 document.location.hash = id; 291 } 292 } 293 }); 294 295 //Loads the +1 button 296 //var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true; 297 //po.src = 'https://apis.google.com/js/plusone.js'; 298 //var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); 299}); 300// END of the onload event 301 302function initExpandableNavItems(rootTag) { 303 var toggleIcon = $( 304 rootTag + ' li.nav-section .nav-section-header .toggle-icon, ' + 305 rootTag + ' li.nav-section .nav-section-header a[href="#"]'); 306 307 toggleIcon.on('click keypress', function(e) { 308 if (e.type == 'keypress' && e.which == 13 || e.type == 'click') { 309 doNavToggle(this); 310 } 311 }); 312 313 // Stop expand/collapse behavior when clicking on nav section links 314 // (since we're navigating away from the page) 315 // This selector captures the first instance of <a>, but not those with "#" as the href. 316 $('.nav-section-header').find('a:eq(0)').not('a[href="#"]').click(function(evt) { 317 window.location.href = $(this).attr('href'); 318 return false; 319 }); 320} 321 322function doNavToggle(el) { 323 var section = $(el).closest('li.nav-section'); 324 if (section.hasClass('expanded')) { 325 /* hide me and descendants */ 326 section.find('ul').slideUp(250, function() { 327 // remove 'expanded' class from my section and any children 328 section.closest('li').removeClass('expanded'); 329 $('li.nav-section', section).removeClass('expanded'); 330 }); 331 } else { 332 /* show me */ 333 // first hide all other siblings 334 var $others = $('li.nav-section.expanded', $(el).closest('ul')).not('.sticky'); 335 $others.removeClass('expanded').children('ul').slideUp(250); 336 337 // now expand me 338 section.closest('li').addClass('expanded'); 339 section.children('ul').slideDown(250); 340 } 341} 342 343/** Highlight the current page in sidenav, expanding children as appropriate */ 344function highlightSidenav() { 345 // if something is already highlighted, undo it. This is for dynamic navigation (Samples index) 346 if ($("ul#nav li.selected").length) { 347 unHighlightSidenav(); 348 } 349 // look for URL in sidenav, including the hash 350 var $selNavLink = $('#nav').find('a[href="' + mPagePath + location.hash + '"]'); 351 352 // If the selNavLink is still empty, look for it without the hash 353 if ($selNavLink.length == 0) { 354 $selNavLink = $('#nav').find('a[href="' + mPagePath + '"]'); 355 } 356 357 var $selListItem; 358 var breadcrumb = []; 359 360 if ($selNavLink.length) { 361 // Find this page's <li> in sidenav and set selected 362 $selListItem = $selNavLink.closest('li'); 363 $selListItem.addClass('selected'); 364 365 // Traverse up the tree and expand all parent nav-sections 366 $selNavLink.parents('li.nav-section').each(function() { 367 $(this).addClass('expanded'); 368 $(this).children('ul').show(); 369 370 var link = $(this).find('a').first(); 371 372 if (!$(this).is($selListItem)) { 373 breadcrumb.unshift(link) 374 } 375 }); 376 377 $('#nav').scrollIntoView($selNavLink); 378 } 379 380 breadcrumb.forEach(function(link) { 381 link.dacCrumbs(); 382 }); 383} 384 385function unHighlightSidenav() { 386 $("ul#nav li.selected").removeClass("selected"); 387 $('ul#nav li.nav-section.expanded').removeClass('expanded').children('ul').hide(); 388} 389 390var agent = navigator['userAgent'].toLowerCase(); 391// If a mobile phone, set flag and do mobile setup 392if ((agent.indexOf("mobile") != -1) || // android, iphone, ipod 393 (agent.indexOf("blackberry") != -1) || 394 (agent.indexOf("webos") != -1) || 395 (agent.indexOf("mini") != -1)) { // opera mini browsers 396 isMobile = true; 397} 398 399$(document).ready(function() { 400 $("pre:not(.no-pretty-print)").addClass("prettyprint"); 401 prettyPrint(); 402}); 403 404/* Show popup dialogs */ 405function showDialog(id) { 406 $dialog = $("#" + id); 407 $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>'); 408 $dialog.wrapInner('<div/>'); 409 $dialog.removeClass("hide"); 410} 411 412/* ######### COOKIES! ########## */ 413 414function readCookie(cookie) { 415 var myCookie = cookie_namespace + "_" + cookie + "="; 416 if (document.cookie) { 417 var index = document.cookie.indexOf(myCookie); 418 if (index != -1) { 419 var valStart = index + myCookie.length; 420 var valEnd = document.cookie.indexOf(";", valStart); 421 if (valEnd == -1) { 422 valEnd = document.cookie.length; 423 } 424 var val = document.cookie.substring(valStart, valEnd); 425 return val; 426 } 427 } 428 return 0; 429} 430 431function writeCookie(cookie, val, section) { 432 if (val == undefined) return; 433 section = section == null ? "_" : "_" + section + "_"; 434 var age = 2 * 365 * 24 * 60 * 60; // set max-age to 2 years 435 var cookieValue = cookie_namespace + section + cookie + "=" + val + 436 "; max-age=" + age + "; path=/"; 437 document.cookie = cookieValue; 438} 439 440/* ######### END COOKIES! ########## */ 441 442/* 443 * Manages secion card states and nav resize to conclude loading 444 */ 445(function() { 446 $(document).ready(function() { 447 448 // Stack hover states 449 $('.section-card-menu').each(function(index, el) { 450 var height = $(el).height(); 451 $(el).css({height:height + 'px', position:'relative'}); 452 var $cardInfo = $(el).find('.card-info'); 453 454 $cardInfo.css({position: 'absolute', bottom:'0px', left:'0px', right:'0px', overflow:'visible'}); 455 }); 456 457 }); 458 459})(); 460 461/* MISC LIBRARY FUNCTIONS */ 462 463function toggle(obj, slide) { 464 var ul = $("ul:first", obj); 465 var li = ul.parent(); 466 if (li.hasClass("closed")) { 467 if (slide) { 468 ul.slideDown("fast"); 469 } else { 470 ul.show(); 471 } 472 li.removeClass("closed"); 473 li.addClass("open"); 474 $(".toggle-img", li).attr("title", "hide pages"); 475 } else { 476 ul.slideUp("fast"); 477 li.removeClass("open"); 478 li.addClass("closed"); 479 $(".toggle-img", li).attr("title", "show pages"); 480 } 481} 482 483function buildToggleLists() { 484 $(".toggle-list").each( 485 function(i) { 486 $("div:first", this).append("<a class='toggle-img' href='#' title='show pages' onClick='toggle(this.parentNode.parentNode, true); return false;'></a>"); 487 $(this).addClass("closed"); 488 }); 489} 490 491function hideNestedItems(list, toggle) { 492 $list = $(list); 493 // hide nested lists 494 if ($list.hasClass('showing')) { 495 $("li ol", $list).hide('fast'); 496 $list.removeClass('showing'); 497 // show nested lists 498 } else { 499 $("li ol", $list).show('fast'); 500 $list.addClass('showing'); 501 } 502 $(".more,.less", $(toggle)).toggle(); 503} 504 505/* Call this to add listeners to a <select> element for Studio/Eclipse/Other docs */ 506function setupIdeDocToggle() { 507 $("select.ide").change(function() { 508 var selected = $(this).find("option:selected").attr("value"); 509 $(".select-ide").hide(); 510 $(".select-ide." + selected).show(); 511 512 $("select.ide").val(selected); 513 }); 514} 515 516/* Used to hide and reveal supplemental content, such as long code samples. 517 See the companion CSS in android-developer-docs.css */ 518function toggleContent(obj) { 519 var div = $(obj).closest(".toggle-content"); 520 var toggleMe = $(".toggle-content-toggleme:eq(0)", div); 521 if (div.hasClass("closed")) { // if it's closed, open it 522 toggleMe.slideDown(); 523 $(".toggle-content-text:eq(0)", obj).toggle(); 524 div.removeClass("closed").addClass("open"); 525 $(".toggle-content-img:eq(0)", div).attr("title", "hide").attr("src", toRoot + 526 "assets/images/styles/disclosure_up.png"); 527 } else { // if it's open, close it 528 toggleMe.slideUp('fast', function() { // Wait until the animation is done before closing arrow 529 $(".toggle-content-text:eq(0)", obj).toggle(); 530 div.removeClass("open").addClass("closed"); 531 div.find(".toggle-content").removeClass("open").addClass("closed") 532 .find(".toggle-content-toggleme").hide(); 533 $(".toggle-content-img", div).attr("title", "show").attr("src", toRoot + 534 "assets/images/styles/disclosure_down.png"); 535 }); 536 } 537 return false; 538} 539 540/* New version of expandable content */ 541function toggleExpandable(link, id) { 542 if ($(id).is(':visible')) { 543 $(id).slideUp(); 544 $(link).removeClass('expanded'); 545 } else { 546 $(id).slideDown(); 547 $(link).addClass('expanded'); 548 } 549} 550 551function hideExpandable(ids) { 552 $(ids).slideUp(); 553 $(ids).prev('h4').find('a.expandable').removeClass('expanded'); 554} 555 556/* 557 * Slideshow 1.0 558 * Used on /index.html and /develop/index.html for carousel 559 * 560 * Sample usage: 561 * HTML - 562 * <div class="slideshow-container"> 563 * <a href="" class="slideshow-prev">Prev</a> 564 * <a href="" class="slideshow-next">Next</a> 565 * <ul> 566 * <li class="item"><img src="images/marquee1.jpg"></li> 567 * <li class="item"><img src="images/marquee2.jpg"></li> 568 * <li class="item"><img src="images/marquee3.jpg"></li> 569 * <li class="item"><img src="images/marquee4.jpg"></li> 570 * </ul> 571 * </div> 572 * 573 * <script type="text/javascript"> 574 * $('.slideshow-container').dacSlideshow({ 575 * auto: true, 576 * btnPrev: '.slideshow-prev', 577 * btnNext: '.slideshow-next' 578 * }); 579 * </script> 580 * 581 * Options: 582 * btnPrev: optional identifier for previous button 583 * btnNext: optional identifier for next button 584 * btnPause: optional identifier for pause button 585 * auto: whether or not to auto-proceed 586 * speed: animation speed 587 * autoTime: time between auto-rotation 588 * easing: easing function for transition 589 * start: item to select by default 590 * scroll: direction to scroll in 591 * pagination: whether or not to include dotted pagination 592 * 593 */ 594 595(function($) { 596 $.fn.dacSlideshow = function(o) { 597 598 //Options - see above 599 o = $.extend({ 600 btnPrev: null, 601 btnNext: null, 602 btnPause: null, 603 auto: true, 604 speed: 500, 605 autoTime: 12000, 606 easing: null, 607 start: 0, 608 scroll: 1, 609 pagination: true 610 611 }, o || {}); 612 613 //Set up a carousel for each 614 return this.each(function() { 615 616 var running = false; 617 var animCss = o.vertical ? "top" : "left"; 618 var sizeCss = o.vertical ? "height" : "width"; 619 var div = $(this); 620 var ul = $("ul", div); 621 var tLi = $("li", ul); 622 var tl = tLi.size(); 623 var timer = null; 624 625 var li = $("li", ul); 626 var itemLength = li.size(); 627 var curr = o.start; 628 629 li.css({float: o.vertical ? "none" : "left"}); 630 ul.css({margin: "0", padding: "0", position: "relative", "list-style-type": "none", "z-index": "1"}); 631 div.css({position: "relative", "z-index": "2", left: "0px"}); 632 633 var liSize = o.vertical ? height(li) : width(li); 634 var ulSize = liSize * itemLength; 635 var divSize = liSize; 636 637 li.css({width: li.width(), height: li.height()}); 638 ul.css(sizeCss, ulSize + "px").css(animCss, -(curr * liSize)); 639 640 div.css(sizeCss, divSize + "px"); 641 642 //Pagination 643 if (o.pagination) { 644 var pagination = $("<div class='pagination'></div>"); 645 var pag_ul = $("<ul></ul>"); 646 if (tl > 1) { 647 for (var i = 0; i < tl; i++) { 648 var li = $("<li>" + i + "</li>"); 649 pag_ul.append(li); 650 if (i == o.start) li.addClass('active'); 651 li.click(function() { 652 go(parseInt($(this).text())); 653 }) 654 } 655 pagination.append(pag_ul); 656 div.append(pagination); 657 } 658 } 659 660 //Previous button 661 if (o.btnPrev) 662 $(o.btnPrev).click(function(e) { 663 e.preventDefault(); 664 return go(curr - o.scroll); 665 }); 666 667 //Next button 668 if (o.btnNext) 669 $(o.btnNext).click(function(e) { 670 e.preventDefault(); 671 return go(curr + o.scroll); 672 }); 673 674 //Pause button 675 if (o.btnPause) 676 $(o.btnPause).click(function(e) { 677 e.preventDefault(); 678 if ($(this).hasClass('paused')) { 679 startRotateTimer(); 680 } else { 681 pauseRotateTimer(); 682 } 683 }); 684 685 //Auto rotation 686 if (o.auto) startRotateTimer(); 687 688 function startRotateTimer() { 689 clearInterval(timer); 690 timer = setInterval(function() { 691 if (curr == tl - 1) { 692 go(0); 693 } else { 694 go(curr + o.scroll); 695 } 696 }, o.autoTime); 697 $(o.btnPause).removeClass('paused'); 698 } 699 700 function pauseRotateTimer() { 701 clearInterval(timer); 702 $(o.btnPause).addClass('paused'); 703 } 704 705 //Go to an item 706 function go(to) { 707 if (!running) { 708 709 if (to < 0) { 710 to = itemLength - 1; 711 } else if (to > itemLength - 1) { 712 to = 0; 713 } 714 curr = to; 715 716 running = true; 717 718 ul.animate( 719 animCss == "left" ? {left: -(curr * liSize)} : {top: -(curr * liSize)} , o.speed, o.easing, 720 function() { 721 running = false; 722 } 723 ); 724 725 $(o.btnPrev + "," + o.btnNext).removeClass("disabled"); 726 $((curr - o.scroll < 0 && o.btnPrev) || 727 (curr + o.scroll > itemLength && o.btnNext) || 728 [] 729 ).addClass("disabled"); 730 731 var nav_items = $('li', pagination); 732 nav_items.removeClass('active'); 733 nav_items.eq(to).addClass('active'); 734 735 } 736 if (o.auto) startRotateTimer(); 737 return false; 738 }; 739 }); 740 }; 741 742 function css(el, prop) { 743 return parseInt($.css(el[0], prop)) || 0; 744 }; 745 function width(el) { 746 return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight'); 747 }; 748 function height(el) { 749 return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom'); 750 }; 751 752})(jQuery); 753 754/* 755 * dacSlideshow 1.0 756 * Used on develop/index.html for side-sliding tabs 757 * 758 * Sample usage: 759 * HTML - 760 * <div class="slideshow-container"> 761 * <a href="" class="slideshow-prev">Prev</a> 762 * <a href="" class="slideshow-next">Next</a> 763 * <ul> 764 * <li class="item"><img src="images/marquee1.jpg"></li> 765 * <li class="item"><img src="images/marquee2.jpg"></li> 766 * <li class="item"><img src="images/marquee3.jpg"></li> 767 * <li class="item"><img src="images/marquee4.jpg"></li> 768 * </ul> 769 * </div> 770 * 771 * <script type="text/javascript"> 772 * $('.slideshow-container').dacSlideshow({ 773 * auto: true, 774 * btnPrev: '.slideshow-prev', 775 * btnNext: '.slideshow-next' 776 * }); 777 * </script> 778 * 779 * Options: 780 * btnPrev: optional identifier for previous button 781 * btnNext: optional identifier for next button 782 * auto: whether or not to auto-proceed 783 * speed: animation speed 784 * autoTime: time between auto-rotation 785 * easing: easing function for transition 786 * start: item to select by default 787 * scroll: direction to scroll in 788 * pagination: whether or not to include dotted pagination 789 * 790 */ 791(function($) { 792 $.fn.dacTabbedList = function(o) { 793 794 //Options - see above 795 o = $.extend({ 796 speed : 250, 797 easing: null, 798 nav_id: null, 799 frame_id: null 800 }, o || {}); 801 802 //Set up a carousel for each 803 return this.each(function() { 804 805 var curr = 0; 806 var running = false; 807 var animCss = "margin-left"; 808 var sizeCss = "width"; 809 var div = $(this); 810 811 var nav = $(o.nav_id, div); 812 var nav_li = $("li", nav); 813 var nav_size = nav_li.size(); 814 var frame = div.find(o.frame_id); 815 var content_width = $(frame).find('ul').width(); 816 //Buttons 817 $(nav_li).click(function(e) { 818 go($(nav_li).index($(this))); 819 }) 820 821 //Go to an item 822 function go(to) { 823 if (!running) { 824 curr = to; 825 running = true; 826 827 frame.animate({'margin-left' : -(curr * content_width)}, o.speed, o.easing, 828 function() { 829 running = false; 830 } 831 ); 832 833 nav_li.removeClass('active'); 834 nav_li.eq(to).addClass('active'); 835 836 } 837 return false; 838 }; 839 }); 840 }; 841 842 function css(el, prop) { 843 return parseInt($.css(el[0], prop)) || 0; 844 }; 845 function width(el) { 846 return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight'); 847 }; 848 function height(el) { 849 return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom'); 850 }; 851 852})(jQuery); 853 854/* ######################################################## */ 855/* ################# JAVADOC REFERENCE ################### */ 856/* ######################################################## */ 857 858 859 860var API_LEVEL_COOKIE = "api_level"; 861var minLevel = 1; 862var maxLevel = 1; 863 864function buildApiLevelSelector() { 865 maxLevel = API_LEVELS.length; 866 var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE)); 867 userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default 868 869 minLevel = parseInt($("#doc-api-level").attr("class")); 870 // Handle provisional api levels; the provisional level will always be the highest possible level 871 // Provisional api levels will also have a length; other stuff that's just missing a level won't, 872 // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class) 873 if (isNaN(minLevel) && minLevel.length) { 874 minLevel = maxLevel; 875 } 876 var select = $("#apiLevelSelector").html("").change(changeApiLevel); 877 for (var i = maxLevel - 1; i >= 0; i--) { 878 var option = $("<option />").attr("value", "" + API_LEVELS[i]).append("" + API_LEVELS[i]); 879 // if (API_LEVELS[i] < minLevel) option.addClass("absent"); // always false for strings (codenames) 880 select.append(option); 881 } 882 883 // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true) 884 var selectedLevelItem = $("#apiLevelSelector option[value='" + userApiLevel + "']").get(0); 885 selectedLevelItem.setAttribute('selected', true); 886} 887 888function changeApiLevel() { 889 maxLevel = API_LEVELS.length; 890 minLevel = parseInt($('#doc-api-level').attr('class')); 891 var selectedLevel = maxLevel; 892 893 selectedLevel = parseInt($("#apiLevelSelector option:selected").val()); 894 toggleVisisbleApis(selectedLevel, "body"); 895 896 writeCookie(API_LEVEL_COOKIE, selectedLevel, null); 897 898 if (selectedLevel < minLevel) { 899 // Show the API notice dialog, set number values and button event 900 $('#api-unavailable').trigger('modal-open'); 901 $('#api-unavailable .selected-level').text(selectedLevel); 902 $('#api-unavailable .api-level').text(minLevel); 903 $('#api-unavailable button.ok').attr('onclick','$("#apiLevelSelector").val("' + minLevel + '");changeApiLevel();'); 904 } 905} 906 907function toggleVisisbleApis(selectedLevel, context) { 908 var apis = $(".api", context); 909 apis.each(function(i) { 910 var obj = $(this); 911 var className = obj.attr("class"); 912 var apiLevelIndex = className.lastIndexOf("-") + 1; 913 var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex); 914 apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length; 915 var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex); 916 if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail 917 return; 918 } 919 apiLevel = parseInt(apiLevel); 920 921 // Handle provisional api levels; if this item's level is the provisional one, set it to the max 922 var selectedLevelNum = parseInt(selectedLevel) 923 var apiLevelNum = parseInt(apiLevel); 924 if (isNaN(apiLevelNum)) { 925 apiLevelNum = maxLevel; 926 } 927 928 // Grey things out that aren't available and give a tooltip title 929 if (apiLevelNum > selectedLevelNum) { 930 obj.addClass("absent").attr("title", "Requires API Level \"" + 931 apiLevel + "\" or higher. To reveal, change the target API level " + 932 "above the left navigation."); 933 } else obj.removeClass("absent").removeAttr("title"); 934 }); 935} 936 937/* ################# SIDENAV TREE VIEW ################### */ 938/* TODO: eliminate redundancy with non-google functions */ 939function init_google_navtree(navtree_id, toroot, root_nodes) { 940 var me = new Object(); 941 me.toroot = toroot; 942 me.node = new Object(); 943 944 me.node.li = document.getElementById(navtree_id); 945 if (!me.node.li) { 946 return; 947 } 948 949 me.node.children_data = root_nodes; 950 me.node.children = new Array(); 951 me.node.children_ul = document.createElement("ul"); 952 me.node.get_children_ul = function() { return me.node.children_ul; }; 953 //me.node.children_ul.className = "children_ul"; 954 me.node.li.appendChild(me.node.children_ul); 955 me.node.depth = 0; 956 957 get_google_node(me, me.node); 958} 959 960function new_google_node(me, mom, text, link, children_data, api_level) { 961 var node = new Object(); 962 var child; 963 node.children = Array(); 964 node.children_data = children_data; 965 node.depth = mom.depth + 1; 966 node.get_children_ul = function() { 967 if (!node.children_ul) { 968 node.children_ul = document.createElement("ul"); 969 node.children_ul.className = "tree-list-children"; 970 node.li.appendChild(node.children_ul); 971 } 972 return node.children_ul; 973 }; 974 node.li = document.createElement("li"); 975 976 mom.get_children_ul().appendChild(node.li); 977 978 if (link) { 979 child = document.createElement("a"); 980 981 } else { 982 child = document.createElement("span"); 983 child.className = "tree-list-subtitle"; 984 985 } 986 if (children_data != null) { 987 node.li.className = "nav-section"; 988 node.label_div = document.createElement("div"); 989 node.label_div.className = "nav-section-header-ref"; 990 node.li.appendChild(node.label_div); 991 get_google_node(me, node); 992 node.label_div.appendChild(child); 993 } else { 994 node.li.appendChild(child); 995 } 996 if (link) { 997 child.href = me.toroot + link; 998 } 999 node.label = document.createTextNode(text); 1000 child.appendChild(node.label); 1001 1002 node.children_ul = null; 1003 1004 return node; 1005} 1006 1007function get_google_node(me, mom) { 1008 mom.children_visited = true; 1009 var linkText; 1010 for (var i in mom.children_data) { 1011 var node_data = mom.children_data[i]; 1012 linkText = node_data[0]; 1013 1014 if (linkText.match("^" + "com.google.android") == "com.google.android") { 1015 linkText = linkText.substr(19, linkText.length); 1016 } 1017 mom.children[i] = new_google_node(me, mom, linkText, node_data[1], 1018 node_data[2], node_data[3]); 1019 } 1020} 1021 1022/****** NEW version of script to build google and sample navs dynamically ******/ 1023// TODO: update Google reference docs to tolerate this new implementation 1024 1025var NODE_NAME = 0; 1026var NODE_HREF = 1; 1027var NODE_GROUP = 2; 1028var NODE_TAGS = 3; 1029var NODE_CHILDREN = 4; 1030 1031function init_google_navtree2(navtree_id, data) { 1032 var $containerUl = $("#" + navtree_id); 1033 for (var i in data) { 1034 var node_data = data[i]; 1035 $containerUl.append(new_google_node2(node_data)); 1036 } 1037 1038 // Make all third-generation list items 'sticky' to prevent them from collapsing 1039 $containerUl.find('li li li.nav-section').addClass('sticky'); 1040 1041 initExpandableNavItems("#" + navtree_id); 1042} 1043 1044function new_google_node2(node_data) { 1045 var linkText = node_data[NODE_NAME]; 1046 if (linkText.match("^" + "com.google.android") == "com.google.android") { 1047 linkText = linkText.substr(19, linkText.length); 1048 } 1049 var $li = $('<li>'); 1050 var $a; 1051 if (node_data[NODE_HREF] != null) { 1052 $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >' + 1053 linkText + '</a>'); 1054 } else { 1055 $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >' + 1056 linkText + '/</a>'); 1057 } 1058 var $childUl = $('<ul>'); 1059 if (node_data[NODE_CHILDREN] != null) { 1060 $li.addClass("nav-section"); 1061 $a = $('<div class="nav-section-header">').append($a); 1062 if (node_data[NODE_HREF] == null) $a.addClass('empty'); 1063 1064 for (var i in node_data[NODE_CHILDREN]) { 1065 var child_node_data = node_data[NODE_CHILDREN][i]; 1066 $childUl.append(new_google_node2(child_node_data)); 1067 } 1068 $li.append($childUl); 1069 } 1070 $li.prepend($a); 1071 1072 return $li; 1073} 1074 1075function showGoogleRefTree() { 1076 init_default_google_navtree(toRoot); 1077 init_default_gcm_navtree(toRoot); 1078} 1079 1080function init_default_google_navtree(toroot) { 1081 // load json file for navtree data 1082 $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) { 1083 // when the file is loaded, initialize the tree 1084 if (jqxhr.status === 200) { 1085 init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA); 1086 highlightSidenav(); 1087 } 1088 }); 1089} 1090 1091function init_default_gcm_navtree(toroot) { 1092 // load json file for navtree data 1093 $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) { 1094 // when the file is loaded, initialize the tree 1095 if (jqxhr.status === 200) { 1096 init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA); 1097 highlightSidenav(); 1098 } 1099 }); 1100} 1101 1102/* TOGGLE INHERITED MEMBERS */ 1103 1104/* Toggle an inherited class (arrow toggle) 1105 * @param linkObj The link that was clicked. 1106 * @param expand 'true' to ensure it's expanded. 'false' to ensure it's closed. 1107 * 'null' to simply toggle. 1108 */ 1109function toggleInherited(linkObj, expand) { 1110 var base = linkObj.getAttribute("id"); 1111 var list = document.getElementById(base + "-list"); 1112 var summary = document.getElementById(base + "-summary"); 1113 var trigger = document.getElementById(base + "-trigger"); 1114 var a = $(linkObj); 1115 if ((expand == null && a.hasClass("closed")) || expand) { 1116 list.style.display = "none"; 1117 summary.style.display = "block"; 1118 trigger.src = toRoot + "assets/images/styles/disclosure_up.png"; 1119 a.removeClass("closed"); 1120 a.addClass("opened"); 1121 } else if ((expand == null && a.hasClass("opened")) || (expand == false)) { 1122 list.style.display = "block"; 1123 summary.style.display = "none"; 1124 trigger.src = toRoot + "assets/images/styles/disclosure_down.png"; 1125 a.removeClass("opened"); 1126 a.addClass("closed"); 1127 } 1128 return false; 1129} 1130 1131/* Toggle all inherited classes in a single table (e.g. all inherited methods) 1132 * @param linkObj The link that was clicked. 1133 * @param expand 'true' to ensure it's expanded. 'false' to ensure it's closed. 1134 * 'null' to simply toggle. 1135 */ 1136function toggleAllInherited(linkObj, expand) { 1137 var a = $(linkObj); 1138 var table = $(a.parent().parent().parent()); // ugly way to get table/tbody 1139 var expandos = $(".jd-expando-trigger", table); 1140 if ((expand == null && a.text() == "[Expand]") || expand) { 1141 expandos.each(function(i) { 1142 toggleInherited(this, true); 1143 }); 1144 a.text("[Collapse]"); 1145 } else if ((expand == null && a.text() == "[Collapse]") || (expand == false)) { 1146 expandos.each(function(i) { 1147 toggleInherited(this, false); 1148 }); 1149 a.text("[Expand]"); 1150 } 1151 return false; 1152} 1153 1154/* Toggle all inherited members in the class (link in the class title) 1155 */ 1156function toggleAllClassInherited() { 1157 var a = $("#toggleAllClassInherited"); // get toggle link from class title 1158 var toggles = $(".toggle-all", $("#body-content")); 1159 if (a.text() == "[Expand All]") { 1160 toggles.each(function(i) { 1161 toggleAllInherited(this, true); 1162 }); 1163 a.text("[Collapse All]"); 1164 } else { 1165 toggles.each(function(i) { 1166 toggleAllInherited(this, false); 1167 }); 1168 a.text("[Expand All]"); 1169 } 1170 return false; 1171} 1172 1173/* Expand all inherited members in the class. Used when initiating page search */ 1174function ensureAllInheritedExpanded() { 1175 var toggles = $(".toggle-all", $("#body-content")); 1176 toggles.each(function(i) { 1177 toggleAllInherited(this, true); 1178 }); 1179 $("#toggleAllClassInherited").text("[Collapse All]"); 1180} 1181 1182/* HANDLE KEY EVENTS 1183 * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search) 1184 */ 1185var agent = navigator['userAgent'].toLowerCase(); 1186var mac = agent.indexOf("macintosh") != -1; 1187 1188$(document).keydown(function(e) { 1189 var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key 1190 if (control && e.which == 70) { // 70 is "F" 1191 ensureAllInheritedExpanded(); 1192 } 1193}); 1194 1195/* On-demand functions */ 1196 1197/** Move sample code line numbers out of PRE block and into non-copyable column */ 1198function initCodeLineNumbers() { 1199 var numbers = $("#codesample-block a.number"); 1200 if (numbers.length) { 1201 $("#codesample-line-numbers").removeClass("hidden").append(numbers); 1202 } 1203 1204 $(document).ready(function() { 1205 // select entire line when clicked 1206 $("span.code-line").click(function() { 1207 if (!shifted) { 1208 selectText(this); 1209 } 1210 }); 1211 // invoke line link on double click 1212 $(".code-line").dblclick(function() { 1213 document.location.hash = $(this).attr('id'); 1214 }); 1215 // highlight the line when hovering on the number 1216 $("#codesample-line-numbers a.number").mouseover(function() { 1217 var id = $(this).attr('href'); 1218 $(id).css('background', '#e7e7e7'); 1219 }); 1220 $("#codesample-line-numbers a.number").mouseout(function() { 1221 var id = $(this).attr('href'); 1222 $(id).css('background', 'none'); 1223 }); 1224 }); 1225} 1226 1227// create SHIFT key binder to avoid the selectText method when selecting multiple lines 1228var shifted = false; 1229$(document).bind('keyup keydown', function(e) { 1230 shifted = e.shiftKey; return true; 1231}); 1232 1233// courtesy of jasonedelman.com 1234function selectText(element) { 1235 var doc = document , 1236 range, selection 1237 ; 1238 if (doc.body.createTextRange) { //ms 1239 range = doc.body.createTextRange(); 1240 range.moveToElementText(element); 1241 range.select(); 1242 } else if (window.getSelection) { //all others 1243 selection = window.getSelection(); 1244 range = doc.createRange(); 1245 range.selectNodeContents(element); 1246 selection.removeAllRanges(); 1247 selection.addRange(range); 1248 } 1249} 1250 1251/** Display links and other information about samples that match the 1252 group specified by the URL */ 1253function showSamples() { 1254 var group = $("#samples").attr('class'); 1255 $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>"); 1256 1257 var $ul = $("<ul>"); 1258 $selectedLi = $("#nav li.selected"); 1259 1260 $selectedLi.children("ul").children("li").each(function() { 1261 var $li = $("<li>").append($(this).find("a").first().clone()); 1262 var $samplesLink = $li.find("a"); 1263 if ($samplesLink.text().endsWith('/')) { 1264 $samplesLink.text($samplesLink.text().slice(0,-1)); 1265 } 1266 $ul.append($li); 1267 }); 1268 1269 $("#samples").append($ul); 1270 1271} 1272 1273/* ########################################################## */ 1274/* ################### RESOURCE CARDS ##################### */ 1275/* ########################################################## */ 1276 1277/** Handle resource queries, collections, and grids (sections). Requires 1278 jd_tag_helpers.js and the *_unified_data.js to be loaded. */ 1279 1280(function() { 1281 $(document).ready(function() { 1282 // Need to initialize hero carousel before other sections for dedupe 1283 // to work correctly. 1284 $('[data-carousel-query]').dacCarouselQuery(); 1285 1286 // Iterate over all instances and initialize a resource widget. 1287 $('.resource-widget').resourceWidget(); 1288 }); 1289 1290 $.fn.widgetOptions = function() { 1291 return { 1292 cardSizes: (this.data('cardsizes') || '').split(','), 1293 maxResults: parseInt(this.data('maxresults'), 10) || Infinity, 1294 initialResults: this.data('initialResults'), 1295 itemsPerPage: this.data('itemsPerPage'), 1296 sortOrder: this.data('sortorder'), 1297 query: this.data('query'), 1298 section: this.data('section'), 1299 /* Added by LFL 6/6/14 */ 1300 resourceStyle: this.data('resourcestyle') || 'card', 1301 stackSort: this.data('stacksort') || 'true', 1302 // For filter based resources 1303 allowDuplicates: this.data('allow-duplicates') || 'false' 1304 }; 1305 }; 1306 1307 $.fn.deprecateOldGridStyles = function() { 1308 var m = this.get(0).className.match(/\bcol-(\d+)\b/); 1309 if (m && !this.is('.cols > *')) { 1310 this.removeClass('col-' + m[1]); 1311 } 1312 return this; 1313 } 1314 1315 /* 1316 * Three types of resource layouts: 1317 * Flow - Uses a fixed row-height flow using float left style. 1318 * Carousel - Single card slideshow all same dimension absolute. 1319 * Stack - Uses fixed columns and flexible element height. 1320 */ 1321 function initResourceWidget(widget, resources, opts) { 1322 var $widget = $(widget).deprecateOldGridStyles(); 1323 var isFlow = $widget.hasClass('resource-flow-layout'); 1324 var isCarousel = $widget.hasClass('resource-carousel-layout'); 1325 var isStack = $widget.hasClass('resource-stack-layout'); 1326 1327 opts = opts || $widget.widgetOptions(); 1328 resources = resources || metadata.query(opts); 1329 1330 if (opts.maxResults !== undefined) { 1331 resources = resources.slice(0, opts.maxResults); 1332 } 1333 1334 if (isFlow) { 1335 drawResourcesFlowWidget($widget, opts, resources); 1336 } else if (isCarousel) { 1337 drawResourcesCarouselWidget($widget, opts, resources); 1338 } else if (isStack) { 1339 opts.numStacks = $widget.data('numstacks'); 1340 drawResourcesStackWidget($widget, opts, resources); 1341 } 1342 } 1343 1344 $.fn.resourceWidget = function(resources, options) { 1345 return this.each(function() { 1346 initResourceWidget(this, resources, options); 1347 }); 1348 }; 1349 1350 /* Initializes a Resource Carousel Widget */ 1351 function drawResourcesCarouselWidget($widget, opts, resources) { 1352 $widget.empty(); 1353 var plusone = false; // stop showing plusone buttons on cards 1354 1355 $widget.addClass('resource-card slideshow-container') 1356 .append($('<a>').addClass('slideshow-prev').text('Prev')) 1357 .append($('<a>').addClass('slideshow-next').text('Next')); 1358 1359 var css = {'width': $widget.width() + 'px', 1360 'height': $widget.height() + 'px'}; 1361 1362 var $ul = $('<ul>'); 1363 1364 for (var i = 0; i < resources.length; ++i) { 1365 var $card = $('<a>') 1366 .attr('href', cleanUrl(resources[i].url)) 1367 .decorateResourceCard(resources[i], plusone); 1368 1369 $('<li>').css(css) 1370 .append($card) 1371 .appendTo($ul); 1372 } 1373 1374 $('<div>').addClass('frame') 1375 .append($ul) 1376 .appendTo($widget); 1377 1378 $widget.dacSlideshow({ 1379 auto: true, 1380 btnPrev: '.slideshow-prev', 1381 btnNext: '.slideshow-next' 1382 }); 1383 } 1384 1385 /* Initializes a Resource Card Stack Widget (column-based layout) 1386 Modified by LFL 6/6/14 1387 */ 1388 function drawResourcesStackWidget($widget, opts, resources, sections) { 1389 // Don't empty widget, grab all items inside since they will be the first 1390 // items stacked, followed by the resource query 1391 var plusone = false; // stop showing plusone buttons on cards 1392 var cards = $widget.find('.resource-card').detach().toArray(); 1393 var numStacks = opts.numStacks || 1; 1394 var $stacks = []; 1395 1396 for (var i = 0; i < numStacks; ++i) { 1397 $stacks[i] = $('<div>').addClass('resource-card-stack') 1398 .appendTo($widget); 1399 } 1400 1401 var sectionResources = []; 1402 1403 // Extract any subsections that are actually resource cards 1404 if (sections) { 1405 for (i = 0; i < sections.length; ++i) { 1406 if (!sections[i].sections || !sections[i].sections.length) { 1407 // Render it as a resource card 1408 sectionResources.push( 1409 $('<a>') 1410 .addClass('resource-card section-card') 1411 .attr('href', cleanUrl(sections[i].resource.url)) 1412 .decorateResourceCard(sections[i].resource, plusone)[0] 1413 ); 1414 1415 } else { 1416 cards.push( 1417 $('<div>') 1418 .addClass('resource-card section-card-menu') 1419 .decorateResourceSection(sections[i], plusone)[0] 1420 ); 1421 } 1422 } 1423 } 1424 1425 cards = cards.concat(sectionResources); 1426 1427 for (i = 0; i < resources.length; ++i) { 1428 var $card = createResourceElement(resources[i], opts); 1429 1430 if (opts.resourceStyle.indexOf('related') > -1) { 1431 $card.addClass('related-card'); 1432 } 1433 1434 cards.push($card[0]); 1435 } 1436 1437 if (opts.stackSort !== 'false') { 1438 for (i = 0; i < cards.length; ++i) { 1439 // Find the stack with the shortest height, but give preference to 1440 // left to right order. 1441 var minHeight = $stacks[0].height(); 1442 var minIndex = 0; 1443 1444 for (var j = 1; j < numStacks; ++j) { 1445 var height = $stacks[j].height(); 1446 if (height < minHeight - 45) { 1447 minHeight = height; 1448 minIndex = j; 1449 } 1450 } 1451 1452 $stacks[minIndex].append($(cards[i])); 1453 } 1454 } 1455 } 1456 1457 /* 1458 Create a resource card using the given resource object and a list of html 1459 configured options. Returns a jquery object containing the element. 1460 */ 1461 function createResourceElement(resource, opts, plusone) { 1462 var $el; 1463 1464 // The difference here is that generic cards are not entirely clickable 1465 // so its a div instead of an a tag, also the generic one is not given 1466 // the resource-card class so it appears with a transparent background 1467 // and can be styled in whatever way the css setup. 1468 if (opts.resourceStyle === 'generic') { 1469 $el = $('<div>') 1470 .addClass('resource') 1471 .attr('href', cleanUrl(resource.url)) 1472 .decorateResource(resource, opts); 1473 } else { 1474 var cls = 'resource resource-card'; 1475 1476 $el = $('<a>') 1477 .addClass(cls) 1478 .attr('href', cleanUrl(resource.url)) 1479 .decorateResourceCard(resource, plusone); 1480 } 1481 1482 return $el; 1483 } 1484 1485 function createResponsiveFlowColumn(cardSize) { 1486 var cardWidth = parseInt(cardSize.match(/(\d+)/)[1], 10); 1487 var column = $('<div>').addClass('col-' + (cardWidth / 3) + 'of6'); 1488 if (cardWidth < 9) { 1489 column.addClass('col-tablet-1of2'); 1490 } else if (cardWidth > 9 && cardWidth < 18) { 1491 column.addClass('col-tablet-1of1'); 1492 } 1493 if (cardWidth < 18) { 1494 column.addClass('col-mobile-1of1'); 1495 } 1496 return column; 1497 } 1498 1499 /* Initializes a flow widget, see distribute.scss for generating accompanying css */ 1500 function drawResourcesFlowWidget($widget, opts, resources) { 1501 // We'll be doing our own modifications to opts. 1502 opts = $.extend({}, opts); 1503 1504 $widget.empty().addClass('cols'); 1505 if (opts.itemsPerPage) { 1506 $('<div class="col-1of1 dac-section-links dac-text-center">') 1507 .append( 1508 $('<div class="dac-section-link dac-show-less" data-toggle="show-less">Less<i class="dac-sprite dac-auto-unfold-less"></i></div>'), 1509 $('<div class="dac-section-link dac-show-more" data-toggle="show-more">More<i class="dac-sprite dac-auto-unfold-more"></i></div>') 1510 ) 1511 .appendTo($widget); 1512 } 1513 1514 $widget.data('options.resourceflow', opts); 1515 $widget.data('resources.resourceflow', resources); 1516 1517 drawResourceFlowPage($widget, opts, resources); 1518 } 1519 1520 function drawResourceFlowPage($widget, opts, resources) { 1521 var cardSizes = opts.cardSizes || ['6x6']; // 2015-08-09: dynamic card sizes are deprecated 1522 var i = opts.currentIndex || 0; 1523 var j = 0; 1524 var plusone = false; // stop showing plusone buttons on cards 1525 var firstPage = i === 0; 1526 var initialResults = opts.initialResults || opts.itemsPerPage || resources.length; 1527 var max = firstPage ? initialResults : i + opts.itemsPerPage; 1528 max = Math.min(resources.length, max); 1529 1530 var page = $('<div class="resource-flow-page">'); 1531 if (opts.itemsPerPage) { 1532 $widget.find('.dac-section-links').before(page); 1533 } else { 1534 $widget.append(page); 1535 } 1536 1537 while (i < max) { 1538 var cardSize = cardSizes[j++ % cardSizes.length]; 1539 cardSize = cardSize.replace(/^\s+|\s+$/, ''); 1540 1541 var column = createResponsiveFlowColumn(cardSize).appendTo(page); 1542 1543 // A stack has a third dimension which is the number of stacked items 1544 var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/); 1545 var stackCount = 0; 1546 var $stackDiv = null; 1547 1548 if (isStack) { 1549 // Create a stack container which should have the dimensions defined 1550 // by the product of the items inside. 1551 $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1] + 1552 'x' + isStack[2] * isStack[3]) .appendTo(column); 1553 } 1554 1555 // Build each stack item or just a single item 1556 do { 1557 var resource = resources[i]; 1558 1559 var $card = createResourceElement(resources[i], opts, plusone); 1560 1561 $card.addClass('resource-card-' + cardSize + 1562 ' resource-card-' + resource.type.toLowerCase()); 1563 1564 if (isStack) { 1565 $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]); 1566 if (++stackCount === parseInt(isStack[3])) { 1567 $card.addClass('resource-card-row-stack-last'); 1568 stackCount = 0; 1569 } 1570 } else { 1571 stackCount = 0; 1572 } 1573 1574 $card.appendTo($stackDiv || column); 1575 1576 } while (++i < max && stackCount > 0); 1577 1578 // Record number of pages viewed in analytics. 1579 if (!firstPage) { 1580 var clicks = Math.ceil((i - initialResults) / opts.itemsPerPage); 1581 devsite.analytics.trackAnalyticsEvent('event', 1582 'Cards', 'Click More', clicks); 1583 } 1584 } 1585 1586 opts.currentIndex = i; 1587 $widget.toggleClass('dac-has-more', i < resources.length); 1588 $widget.toggleClass('dac-has-less', !firstPage); 1589 1590 $widget.trigger('dac:domchange'); 1591 if (opts.onRenderPage) { 1592 opts.onRenderPage(page); 1593 } 1594 } 1595 1596 function drawResourceFlowReset($widget, opts, resources) { 1597 $widget.find('.resource-flow-page') 1598 .slice(1) 1599 .remove(); 1600 $widget.toggleClass('dac-has-more', true); 1601 $widget.toggleClass('dac-has-less', false); 1602 1603 opts.currentIndex = Math.min(opts.initialResults, resources.length); 1604 devsite.analytics.trackAnalyticsEvent('event', 'Cards', 'Click Less'); 1605 } 1606 1607 /* A decorator for event functions which finds the surrounding widget and it's options */ 1608 function wrapWithWidget(func) { 1609 return function(e) { 1610 if (e) e.preventDefault(); 1611 1612 var $widget = $(this).closest('.resource-flow-layout'); 1613 var opts = $widget.data('options.resourceflow'); 1614 var resources = $widget.data('resources.resourceflow'); 1615 func($widget, opts, resources); 1616 }; 1617 } 1618 1619 /* Build a site map of resources using a section as a root. */ 1620 function buildSectionList(opts) { 1621 if (opts.section && SECTION_BY_ID[opts.section]) { 1622 return SECTION_BY_ID[opts.section].sections || []; 1623 } 1624 return []; 1625 } 1626 1627 function cleanUrl(url) { 1628 if (url && url.indexOf('//') === -1) { 1629 url = toRoot + url; 1630 } 1631 1632 return url; 1633 } 1634 1635 // Delegated events for resources. 1636 $(document).on('click', '.resource-flow-layout [data-toggle="show-more"]', wrapWithWidget(drawResourceFlowPage)); 1637 $(document).on('click', '.resource-flow-layout [data-toggle="show-less"]', wrapWithWidget(drawResourceFlowReset)); 1638})(); 1639 1640(function($) { 1641 // A mapping from category and type values to new values or human presentable strings. 1642 var SECTION_MAP = { 1643 googleplay: 'google play' 1644 }; 1645 1646 /* 1647 Utility method for creating dom for the description area of a card. 1648 Used in decorateResourceCard and decorateResource. 1649 */ 1650 function buildResourceCardDescription(resource, plusone) { 1651 var $description = $('<div>').addClass('description ellipsis'); 1652 1653 $description.append($('<div>').addClass('text').html(resource.summary)); 1654 1655 if (resource.cta) { 1656 $description.append($('<a>').addClass('cta').html(resource.cta)); 1657 } 1658 1659 if (plusone) { 1660 var plusurl = resource.url.indexOf("//") > -1 ? resource.url : 1661 "//developer.android.com/" + resource.url; 1662 1663 $description.append($('<div>').addClass('util') 1664 .append($('<div>').addClass('g-plusone') 1665 .attr('data-size', 'small') 1666 .attr('data-align', 'right') 1667 .attr('data-href', plusurl))); 1668 } 1669 1670 return $description; 1671 } 1672 1673 /* Simple jquery function to create dom for a standard resource card */ 1674 $.fn.decorateResourceCard = function(resource, plusone) { 1675 var section = resource.category || resource.type; 1676 section = (SECTION_MAP[section] || section).toLowerCase(); 1677 var imgUrl = resource.image || 1678 'assets/images/resource-card-default-android.jpg'; 1679 1680 if (imgUrl.indexOf('//') === -1) { 1681 imgUrl = toRoot + imgUrl; 1682 } 1683 1684 if (resource.type === 'youtube' || resource.type === 'video') { 1685 $('<div>').addClass('play-button') 1686 .append($('<i class="dac-sprite dac-play-white">')) 1687 .appendTo(this); 1688 } 1689 1690 $('<div>').addClass('card-bg') 1691 .css('background-image', 'url(' + (imgUrl || toRoot + 1692 'assets/images/resource-card-default-android.jpg') + ')') 1693 .appendTo(this); 1694 1695 $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : '')) 1696 .append($('<div>').addClass('section').text(section)) 1697 .append($('<div>').addClass('title' + (resource.title_highlighted ? ' highlighted' : '')) 1698 .html(resource.title_highlighted || resource.title)) 1699 .append(buildResourceCardDescription(resource, plusone)) 1700 .appendTo(this); 1701 1702 return this; 1703 }; 1704 1705 /* Simple jquery function to create dom for a resource section card (menu) */ 1706 $.fn.decorateResourceSection = function(section, plusone) { 1707 var resource = section.resource; 1708 //keep url clean for matching and offline mode handling 1709 var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot; 1710 var $base = $('<a>') 1711 .addClass('card-bg') 1712 .attr('href', resource.url) 1713 .append($('<div>').addClass('card-section-icon') 1714 .append($('<div>').addClass('icon')) 1715 .append($('<div>').addClass('section').html(resource.title))) 1716 .appendTo(this); 1717 1718 var $cardInfo = $('<div>').addClass('card-info').appendTo(this); 1719 1720 if (section.sections && section.sections.length) { 1721 // Recurse the section sub-tree to find a resource image. 1722 var stack = [section]; 1723 1724 while (stack.length) { 1725 if (stack[0].resource.image) { 1726 $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')'); 1727 break; 1728 } 1729 1730 if (stack[0].sections) { 1731 stack = stack.concat(stack[0].sections); 1732 } 1733 1734 stack.shift(); 1735 } 1736 1737 var $ul = $('<ul>') 1738 .appendTo($cardInfo); 1739 1740 var max = section.sections.length > 3 ? 3 : section.sections.length; 1741 1742 for (var i = 0; i < max; ++i) { 1743 1744 var subResource = section.sections[i]; 1745 if (!plusone) { 1746 $('<li>') 1747 .append($('<a>').attr('href', subResource.url) 1748 .append($('<div>').addClass('title').html(subResource.title)) 1749 .append($('<div>').addClass('description ellipsis') 1750 .append($('<div>').addClass('text').html(subResource.summary)) 1751 .append($('<div>').addClass('util')))) 1752 .appendTo($ul); 1753 } else { 1754 $('<li>') 1755 .append($('<a>').attr('href', subResource.url) 1756 .append($('<div>').addClass('title').html(subResource.title)) 1757 .append($('<div>').addClass('description ellipsis') 1758 .append($('<div>').addClass('text').html(subResource.summary)) 1759 .append($('<div>').addClass('util') 1760 .append($('<div>').addClass('g-plusone') 1761 .attr('data-size', 'small') 1762 .attr('data-align', 'right') 1763 .attr('data-href', resource.url))))) 1764 .appendTo($ul); 1765 } 1766 } 1767 1768 // Add a more row 1769 if (max < section.sections.length) { 1770 $('<li>') 1771 .append($('<a>').attr('href', resource.url) 1772 .append($('<div>') 1773 .addClass('title') 1774 .text('More'))) 1775 .appendTo($ul); 1776 } 1777 } else { 1778 // No sub-resources, just render description? 1779 } 1780 1781 return this; 1782 }; 1783 1784 /* Render other types of resource styles that are not cards. */ 1785 $.fn.decorateResource = function(resource, opts) { 1786 var imgUrl = resource.image || 1787 'assets/images/resource-card-default-android.jpg'; 1788 var linkUrl = resource.url; 1789 1790 if (imgUrl.indexOf('//') === -1) { 1791 imgUrl = toRoot + imgUrl; 1792 } 1793 1794 if (linkUrl && linkUrl.indexOf('//') === -1) { 1795 linkUrl = toRoot + linkUrl; 1796 } 1797 1798 $(this).append( 1799 $('<div>').addClass('image') 1800 .css('background-image', 'url(' + imgUrl + ')'), 1801 $('<div>').addClass('info').append( 1802 $('<h4>').addClass('title').html(resource.title_highlighted || resource.title), 1803 $('<p>').addClass('summary').html(resource.summary), 1804 $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More') 1805 ) 1806 ); 1807 1808 return this; 1809 }; 1810})(jQuery); 1811 1812/* 1813 Fullscreen Carousel 1814 1815 The following allows for an area at the top of the page that takes over the 1816 entire browser height except for its top offset and an optional bottom 1817 padding specified as a data attribute. 1818 1819 HTML: 1820 1821 <div class="fullscreen-carousel"> 1822 <div class="fullscreen-carousel-content"> 1823 <!-- content here --> 1824 </div> 1825 <div class="fullscreen-carousel-content"> 1826 <!-- content here --> 1827 </div> 1828 1829 etc ... 1830 1831 </div> 1832 1833 Control over how the carousel takes over the screen can mostly be defined in 1834 a css file. Setting min-height on the .fullscreen-carousel-content elements 1835 will prevent them from shrinking to far vertically when the browser is very 1836 short, and setting max-height on the .fullscreen-carousel itself will prevent 1837 the area from becoming to long in the case that the browser is stretched very 1838 tall. 1839 1840 There is limited functionality for having multiple sections since that request 1841 was removed, but it is possible to add .next-arrow and .prev-arrow elements to 1842 scroll between multiple content areas. 1843*/ 1844 1845(function() { 1846 $(document).ready(function() { 1847 $('.fullscreen-carousel').each(function() { 1848 initWidget(this); 1849 }); 1850 }); 1851 1852 function initWidget(widget) { 1853 var $widget = $(widget); 1854 1855 var topOffset = $widget.offset().top; 1856 var padBottom = parseInt($widget.data('paddingbottom')) || 0; 1857 var maxHeight = 0; 1858 var minHeight = 0; 1859 var $content = $widget.find('.fullscreen-carousel-content'); 1860 var $nextArrow = $widget.find('.next-arrow'); 1861 var $prevArrow = $widget.find('.prev-arrow'); 1862 var $curSection = $($content[0]); 1863 1864 if ($content.length <= 1) { 1865 $nextArrow.hide(); 1866 $prevArrow.hide(); 1867 } else { 1868 $nextArrow.click(function() { 1869 var index = ($content.index($curSection) + 1); 1870 $curSection.hide(); 1871 $curSection = $($content[index >= $content.length ? 0 : index]); 1872 $curSection.show(); 1873 }); 1874 1875 $prevArrow.click(function() { 1876 var index = ($content.index($curSection) - 1); 1877 $curSection.hide(); 1878 $curSection = $($content[index < 0 ? $content.length - 1 : 0]); 1879 $curSection.show(); 1880 }); 1881 } 1882 1883 // Just hide all content sections except first. 1884 $content.each(function(index) { 1885 if ($(this).height() > minHeight) minHeight = $(this).height(); 1886 $(this).css({position: 'absolute', display: index > 0 ? 'none' : ''}); 1887 }); 1888 1889 // Register for changes to window size, and trigger. 1890 $(window).resize(resizeWidget); 1891 resizeWidget(); 1892 1893 function resizeWidget() { 1894 var height = $(window).height() - topOffset - padBottom; 1895 $widget.width($(window).width()); 1896 $widget.height(height < minHeight ? minHeight : 1897 (maxHeight && height > maxHeight ? maxHeight : height)); 1898 } 1899 } 1900})(); 1901 1902/* 1903 Tab Carousel 1904 1905 The following allows tab widgets to be installed via the html below. Each 1906 tab content section should have a data-tab attribute matching one of the 1907 nav items'. Also each tab content section should have a width matching the 1908 tab carousel. 1909 1910 HTML: 1911 1912 <div class="tab-carousel"> 1913 <ul class="tab-nav"> 1914 <li><a href="#" data-tab="handsets">Handsets</a> 1915 <li><a href="#" data-tab="wearable">Wearable</a> 1916 <li><a href="#" data-tab="tv">TV</a> 1917 </ul> 1918 1919 <div class="tab-carousel-content"> 1920 <div data-tab="handsets"> 1921 <!--Full width content here--> 1922 </div> 1923 1924 <div data-tab="wearable"> 1925 <!--Full width content here--> 1926 </div> 1927 1928 <div data-tab="tv"> 1929 <!--Full width content here--> 1930 </div> 1931 </div> 1932 </div> 1933 1934*/ 1935(function() { 1936 $(document).ready(function() { 1937 $('.tab-carousel').each(function() { 1938 initWidget(this); 1939 }); 1940 }); 1941 1942 function initWidget(widget) { 1943 var $widget = $(widget); 1944 var $nav = $widget.find('.tab-nav'); 1945 var $anchors = $nav.find('[data-tab]'); 1946 var $li = $nav.find('li'); 1947 var $contentContainer = $widget.find('.tab-carousel-content'); 1948 var $tabs = $contentContainer.find('[data-tab]'); 1949 var $curTab = $($tabs[0]); // Current tab is first tab. 1950 var width = $widget.width(); 1951 1952 // Setup nav interactivity. 1953 $anchors.click(function(evt) { 1954 evt.preventDefault(); 1955 var query = '[data-tab=' + $(this).data('tab') + ']'; 1956 transitionWidget($tabs.filter(query)); 1957 }); 1958 1959 // Add highlight for navigation on first item. 1960 var $highlight = $('<div>').addClass('highlight') 1961 .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'}) 1962 .appendTo($nav); 1963 1964 // Store height since we will change contents to absolute. 1965 $contentContainer.height($contentContainer.height()); 1966 1967 // Absolutely position tabs so they're ready for transition. 1968 $tabs.each(function(index) { 1969 $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'}); 1970 }); 1971 1972 function transitionWidget($toTab) { 1973 if (!$curTab.is($toTab)) { 1974 var curIndex = $tabs.index($curTab[0]); 1975 var toIndex = $tabs.index($toTab[0]); 1976 var dir = toIndex > curIndex ? 1 : -1; 1977 1978 // Animate content sections. 1979 $toTab.css({left:(width * dir) + 'px'}); 1980 $curTab.animate({left:(width * -dir) + 'px'}); 1981 $toTab.animate({left:'0'}); 1982 1983 // Animate navigation highlight. 1984 $highlight.animate({left:$($li[toIndex]).position().left + 'px', 1985 width:$($li[toIndex]).outerWidth() + 'px'}) 1986 1987 // Store new current section. 1988 $curTab = $toTab; 1989 } 1990 } 1991 } 1992})(); 1993 1994/** 1995 * Auto TOC 1996 * 1997 * Upgrades h2s on the page to have a rule and be toggle-able on mobile. 1998 */ 1999(function($) { 2000 var upgraded = false; 2001 var h2Titles; 2002 2003 function initWidget() { 2004 // add HRs below all H2s (except for a few other h2 variants) 2005 // Consider doing this with css instead. 2006 h2Titles = $('h2').not('#qv h2, #tb h2, .sidebox h2, #devdoc-nav h2, h2.norule'); 2007 h2Titles.css({paddingBottom:0}).after('<hr/>'); 2008 2009 // Exit early if on older browser. 2010 if (!window.matchMedia) { 2011 return; 2012 } 2013 2014 // Only run logic in mobile layout. 2015 var query = window.matchMedia('(max-width: 719px)'); 2016 if (query.matches) { 2017 makeTogglable(); 2018 } else { 2019 query.addListener(makeTogglable); 2020 } 2021 } 2022 2023 function makeTogglable() { 2024 // Only run this logic once. 2025 if (upgraded) { return; } 2026 upgraded = true; 2027 2028 // Only make content h2s togglable. 2029 var contentTitles = h2Titles.filter('#jd-content *'); 2030 2031 // If there are more than 1 2032 if (contentTitles.size() < 2) { 2033 return; 2034 } 2035 2036 contentTitles.each(function() { 2037 // Find all the relevant nodes. 2038 var $title = $(this); 2039 var $hr = $title.next(); 2040 var $contents = allNextUntil($hr[0], 'h2, .next-docs'); 2041 var $section = $($title) 2042 .add($hr) 2043 .add($title.prev('a[name]')) 2044 .add($contents); 2045 var $anchor = $section.first().prev(); 2046 var anchorMethod = 'after'; 2047 if ($anchor.length === 0) { 2048 $anchor = $title.parent(); 2049 anchorMethod = 'prepend'; 2050 } 2051 2052 // Some h2s are in their own container making it pretty hard to find the end, so skip. 2053 if ($contents.length === 0) { 2054 return; 2055 } 2056 2057 // Remove from DOM before messing with it. DOM is slow! 2058 $section.detach(); 2059 2060 // Add mobile-only expand arrows. 2061 $title.prepend('<span class="dac-visible-mobile-inline-block">' + 2062 '<i class="dac-toggle-expand dac-sprite dac-expand-more-black"></i>' + 2063 '<i class="dac-toggle-collapse dac-sprite dac-expand-less-black"></i>' + 2064 '</span>') 2065 .attr('data-toggle', 'section'); 2066 2067 // Wrap in magic markup. 2068 $section = $section.wrapAll('<div class="dac-toggle dac-mobile">').parent(); 2069 2070 // extra div used for max-height calculation. 2071 $contents.wrapAll('<div class="dac-toggle-content dac-expand"><div>'); 2072 2073 // Pre-expand section if requested. 2074 if ($title.hasClass('is-expanded')) { 2075 $section.addClass('is-expanded'); 2076 } 2077 2078 // Pre-expand section if targetted by hash. 2079 if (location.hash && $section.find(location.hash).length) { 2080 $section.addClass('is-expanded'); 2081 } 2082 2083 // Add it back to the dom. 2084 $anchor[anchorMethod].call($anchor, $section); 2085 }); 2086 } 2087 2088 // Similar to $.fn.nextUntil() except we need all nodes, jQuery skips text nodes. 2089 function allNextUntil(elem, until) { 2090 var matched = []; 2091 2092 while ((elem = elem.nextSibling) && elem.nodeType !== 9) { 2093 if (elem.nodeType === 1 && jQuery(elem).is(until)) { 2094 break; 2095 } 2096 matched.push(elem); 2097 } 2098 return $(matched); 2099 } 2100 2101 $(function() { 2102 initWidget(); 2103 }); 2104})(jQuery); 2105 2106(function($, window) { 2107 'use strict'; 2108 2109 // Blogger API info 2110 var apiUrl = 'https://www.googleapis.com/blogger/v3'; 2111 var apiKey = 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8'; 2112 2113 // Blog IDs can be found in the markup of the blog posts 2114 var blogs = { 2115 'android-developers': { 2116 id: '6755709643044947179', 2117 title: 'Android Developers Blog' 2118 } 2119 }; 2120 var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 2121 'July', 'August', 'September', 'October', 'November', 'December']; 2122 2123 var BlogReader = (function() { 2124 var reader; 2125 2126 function BlogReader() { 2127 this.doneSetup = false; 2128 } 2129 2130 /** 2131 * Initialize the blog reader and modal. 2132 */ 2133 BlogReader.prototype.setup = function() { 2134 $('#jd-content').append( 2135 '<div id="blog-reader" data-modal="blog-reader" class="dac-modal dac-has-small-header">' + 2136 '<div class="dac-modal-container">' + 2137 '<div class="dac-modal-window">' + 2138 '<header class="dac-modal-header">' + 2139 '<div class="dac-modal-header-actions">' + 2140 '<a href="" class="dac-modal-header-open" target="_blank">' + 2141 '<i class="dac-sprite dac-open-in-new"></i>' + 2142 '</a>' + 2143 '<button class="dac-modal-header-close" data-modal-toggle>' + 2144 '</button>' + 2145 '</div>' + 2146 '<h2 class="norule dac-modal-header-title"></h2>' + 2147 '</header>' + 2148 '<div class="dac-modal-content dac-blog-reader">' + 2149 '<time class="dac-blog-reader-date" pubDate></time>' + 2150 '<h3 class="dac-blog-reader-title"></h3>' + 2151 '<div class="dac-blog-reader-text clearfix"></div>' + 2152 '</div>' + 2153 '</div>' + 2154 '</div>' + 2155 '</div>'); 2156 2157 this.blogReader = $('#blog-reader').dacModal(); 2158 2159 this.doneSetup = true; 2160 }; 2161 2162 BlogReader.prototype.openModal_ = function(blog, post) { 2163 var published = new Date(post.published); 2164 var formattedDate = monthNames[published.getMonth()] + ' ' + published.getDate() + ' ' + published.getFullYear(); 2165 this.blogReader.find('.dac-modal-header-open').attr('href', post.url); 2166 this.blogReader.find('.dac-modal-header-title').text(blog.title); 2167 this.blogReader.find('.dac-blog-reader-title').html(post.title); 2168 this.blogReader.find('.dac-blog-reader-date').html(formattedDate); 2169 this.blogReader.find('.dac-blog-reader-text').html(post.content); 2170 this.blogReader.trigger('modal-open'); 2171 }; 2172 2173 /** 2174 * Show a blog post in a modal 2175 * @param {string} blogName - The name of the Blogspot blog. 2176 * @param {string} postPath - The path to the blog post. 2177 * @param {bool} secondTry - Has it failed once? 2178 */ 2179 BlogReader.prototype.showPost = function(blogName, postPath, secondTry) { 2180 var blog = blogs[blogName]; 2181 var postUrl = 'https://' + blogName + '.blogspot.com' + postPath; 2182 2183 var url = apiUrl + '/blogs/' + blog.id + '/posts/bypath?path=' + encodeURIComponent(postPath) + '&key=' + apiKey; 2184 $.ajax(url, {timeout: 650}).done(this.openModal_.bind(this, blog)).fail(function(error) { 2185 // Retry once if we get an error 2186 if (error.status === 500 && !secondTry) { 2187 this.showPost(blogName, postPath, true); 2188 } else { 2189 window.location.href = postUrl; 2190 } 2191 }.bind(this)); 2192 }; 2193 2194 return { 2195 getReader: function() { 2196 if (!reader) { 2197 reader = new BlogReader(); 2198 } 2199 return reader; 2200 } 2201 }; 2202 })(); 2203 2204 var blogReader = BlogReader.getReader(); 2205 2206 function wrapLinkWithReader(e) { 2207 var el = $(e.currentTarget); 2208 if (el.hasClass('dac-modal-header-open')) { 2209 return; 2210 } 2211 2212 // Only catch links on blogspot.com 2213 var matches = el.attr('href').match(/https?:\/\/([^\.]*).blogspot.com([^$]*)/); 2214 if (matches && matches.length === 3) { 2215 var blogName = matches[1]; 2216 var postPath = matches[2]; 2217 2218 // Check if we have information about the blog 2219 if (!blogs[blogName]) { 2220 return; 2221 } 2222 2223 // Setup the first time it's used 2224 if (!blogReader.doneSetup) { 2225 blogReader.setup(); 2226 } 2227 2228 e.preventDefault(); 2229 blogReader.showPost(blogName, postPath); 2230 } 2231 } 2232 2233 $(document).on('click.blog-reader', 'a.resource-card[href*="blogspot.com/"]', 2234 wrapLinkWithReader); 2235})(jQuery, window); 2236 2237(function($) { 2238 $.fn.debounce = function(func, wait, immediate) { 2239 var timeout; 2240 2241 return function() { 2242 var context = this; 2243 var args = arguments; 2244 2245 var later = function() { 2246 timeout = null; 2247 if (!immediate) { 2248 func.apply(context, args); 2249 } 2250 }; 2251 2252 var callNow = immediate && !timeout; 2253 clearTimeout(timeout); 2254 timeout = setTimeout(later, wait); 2255 2256 if (callNow) { 2257 func.apply(context, args); 2258 } 2259 }; 2260 }; 2261})(jQuery); 2262 2263/* Calculate the vertical area remaining */ 2264(function($) { 2265 $.fn.ellipsisfade = function() { 2266 // Only fetch line-height of first element to avoid recalculate style. 2267 // Will be NaN if no elements match, which is ok. 2268 var lineHeight = parseInt(this.css('line-height'), 10); 2269 2270 this.each(function() { 2271 // get element text 2272 var $this = $(this); 2273 var remainingHeight = $this.parent().parent().height(); 2274 $this.parent().siblings().each(function() { 2275 var elHeight; 2276 if ($(this).is(':visible')) { 2277 elHeight = $(this).outerHeight(true); 2278 remainingHeight = remainingHeight - elHeight; 2279 } 2280 }); 2281 2282 var adjustedRemainingHeight = ((remainingHeight) / lineHeight >> 0) * lineHeight; 2283 $this.parent().css({height: adjustedRemainingHeight}); 2284 $this.css({height: 'auto'}); 2285 }); 2286 2287 return this; 2288 }; 2289 2290 /* Pass the line height to ellipsisfade() to adjust the height of the 2291 text container to show the max number of lines possible, without 2292 showing lines that are cut off. This works with the css ellipsis 2293 classes to fade last text line and apply an ellipsis char. */ 2294 function updateEllipsis(context) { 2295 if (!(context instanceof jQuery)) { 2296 context = $('html'); 2297 } 2298 2299 context.find('.card-info .text').ellipsisfade(); 2300 } 2301 2302 $(window).on('resize', $.fn.debounce(updateEllipsis, 500)); 2303 $(updateEllipsis); 2304 $('html').on('dac:domchange', function(e) { updateEllipsis($(e.target)); }); 2305})(jQuery); 2306 2307/* Filter */ 2308(function($) { 2309 'use strict'; 2310 2311 /** 2312 * A single filter item content. 2313 * @type {string} - Element template. 2314 * @private 2315 */ 2316 var ITEM_STR_ = '<input type="checkbox" value="{{value}}" class="dac-form-checkbox" id="{{id}}">' + 2317 '<label for="{{id}}" class="dac-form-checkbox-button"></label>' + 2318 '<label for="{{id}}" class="dac-form-label">{{name}}</label>'; 2319 2320 /** 2321 * Template for a chip element. 2322 * @type {*|HTMLElement} 2323 * @private 2324 */ 2325 var CHIP_BASE_ = $('<li class="dac-filter-chip">' + 2326 '<button class="dac-filter-chip-close">' + 2327 '<i class="dac-sprite dac-close-black dac-filter-chip-close-icon"></i>' + 2328 '</button>' + 2329 '</li>'); 2330 2331 /** 2332 * Component to handle narrowing down resources. 2333 * @param {HTMLElement} el - The DOM element. 2334 * @param {Object} options 2335 * @constructor 2336 */ 2337 function Filter(el, options) { 2338 this.el = $(el); 2339 this.options = $.extend({}, Filter.DEFAULTS_, options); 2340 this.init(); 2341 } 2342 2343 Filter.DEFAULTS_ = { 2344 activeClass: 'dac-active', 2345 chipsDataAttr: 'filter-chips', 2346 nameDataAttr: 'filter-name', 2347 countDataAttr: 'filter-count', 2348 tabViewDataAttr: 'tab-view', 2349 valueDataAttr: 'filter-value' 2350 }; 2351 2352 /** 2353 * Draw resource cards. 2354 * @param {Array} resources 2355 * @private 2356 */ 2357 Filter.prototype.draw_ = function(resources) { 2358 var that = this; 2359 2360 if (resources.length === 0) { 2361 this.containerEl_.html('<p class="dac-filter-message">Nothing matches selected filters.</p>'); 2362 return; 2363 } 2364 2365 // Draw resources. 2366 that.containerEl_.resourceWidget(resources, that.data_.options); 2367 }; 2368 2369 /** 2370 * Initialize a Filter component. 2371 */ 2372 Filter.prototype.init = function() { 2373 this.containerEl_ = $(this.options.filter); 2374 2375 // Setup data settings 2376 this.data_ = {}; 2377 this.data_.chips = {}; 2378 this.data_.options = this.containerEl_.widgetOptions(); 2379 this.data_.all = window.metadata.query(this.data_.options); 2380 2381 // Initialize filter UI 2382 this.initUi(); 2383 }; 2384 2385 /** 2386 * Generate a chip for a given filter item. 2387 * @param {Object} item - A single filter option (checkbox container). 2388 * @returns {HTMLElement} A new Chip element. 2389 */ 2390 Filter.prototype.chipForItem = function(item) { 2391 var chip = CHIP_BASE_.clone(); 2392 chip.prepend(this.data_.chips[item.data('filter-value')]); 2393 chip.data('item.dac-filter', item); 2394 item.data('chip.dac-filter', chip); 2395 this.addToItemValue(item, 1); 2396 return chip[0]; 2397 }; 2398 2399 /** 2400 * Update count of checked filter items. 2401 * @param {Object} item - A single filter option (checkbox container). 2402 * @param {Number} value - Either -1 or 1. 2403 */ 2404 Filter.prototype.addToItemValue = function(item, value) { 2405 var tab = item.parent().data(this.options.tabViewDataAttr); 2406 var countEl = this.countEl_.filter('[data-' + this.options.countDataAttr + '="' + tab + '"]'); 2407 var count = value + parseInt(countEl.text(), 10); 2408 countEl.text(count); 2409 countEl.toggleClass('dac-disabled', count === 0); 2410 }; 2411 2412 /** 2413 * Set event listeners. 2414 * @private 2415 */ 2416 Filter.prototype.setEventListeners_ = function() { 2417 this.chipsEl_.on('click.dac-filter', '.dac-filter-chip-close', this.closeChipHandler_.bind(this)); 2418 this.tabViewEl_.on('change.dac-filter', ':checkbox', this.toggleCheckboxHandler_.bind(this)); 2419 }; 2420 2421 /** 2422 * Check filter items that are active by default. 2423 */ 2424 Filter.prototype.activateInitialFilters_ = function() { 2425 var id = (new Date()).getTime(); 2426 var initiallyCheckedValues = this.data_.options.query.replace(/,\s*/g, '+').split('+'); 2427 var chips = document.createDocumentFragment(); 2428 var that = this; 2429 2430 this.items_.each(function(i) { 2431 var item = $(this); 2432 var opts = item.data(); 2433 that.data_.chips[opts.filterValue] = opts.filterName; 2434 2435 var checkbox = $(ITEM_STR_.replace(/\{\{name\}\}/g, opts.filterName) 2436 .replace(/\{\{value\}\}/g, opts.filterValue) 2437 .replace(/\{\{id\}\}/g, 'filter-' + id + '-' + (i + 1))); 2438 2439 if (initiallyCheckedValues.indexOf(opts.filterValue) > -1) { 2440 checkbox[0].checked = true; 2441 chips.appendChild(that.chipForItem(item)); 2442 } 2443 2444 item.append(checkbox); 2445 }); 2446 2447 this.chipsEl_.append(chips); 2448 }; 2449 2450 /** 2451 * Initialize the Filter view 2452 */ 2453 Filter.prototype.initUi = function() { 2454 // Cache DOM elements 2455 this.chipsEl_ = this.el.find('[data-' + this.options.chipsDataAttr + ']'); 2456 this.countEl_ = this.el.find('[data-' + this.options.countDataAttr + ']'); 2457 this.tabViewEl_ = this.el.find('[data-' + this.options.tabViewDataAttr + ']'); 2458 this.items_ = this.el.find('[data-' + this.options.nameDataAttr + ']'); 2459 2460 // Setup UI 2461 this.draw_(this.data_.all); 2462 this.activateInitialFilters_(); 2463 this.setEventListeners_(); 2464 }; 2465 2466 /** 2467 * @returns {[types|Array, tags|Array, category|Array]} 2468 */ 2469 Filter.prototype.getActiveClauses = function() { 2470 var tags = []; 2471 var types = []; 2472 var categories = []; 2473 2474 this.items_.find(':checked').each(function(i, checkbox) { 2475 // Currently, there is implicit business logic here that `tag` is AND'ed together 2476 // while `type` is OR'ed. So , and + do the same thing here. It would be great to 2477 // reuse the same query engine for filters, but it would need more powerful syntax. 2478 // Probably parenthesis, to support "tag:dog + tag:cat + (type:video, type:blog)" 2479 var expression = $(checkbox).val(); 2480 var regex = /(\w+):(\w+)/g; 2481 var match; 2482 2483 while (match = regex.exec(expression)) { 2484 switch (match[1]) { 2485 case 'category': 2486 categories.push(match[2]); 2487 break; 2488 case 'tag': 2489 tags.push(match[2]); 2490 break; 2491 case 'type': 2492 types.push(match[2]); 2493 break; 2494 } 2495 } 2496 }); 2497 2498 return [types, tags, categories]; 2499 }; 2500 2501 /** 2502 * Actual filtering logic. 2503 * @returns {Array} 2504 */ 2505 Filter.prototype.filteredResources = function() { 2506 var data = this.getActiveClauses(); 2507 var types = data[0]; 2508 var tags = data[1]; 2509 var categories = data[2]; 2510 var resources = []; 2511 var resource = {}; 2512 var tag = ''; 2513 var shouldAddResource = true; 2514 2515 for (var resourceIndex = 0; resourceIndex < this.data_.all.length; resourceIndex++) { 2516 resource = this.data_.all[resourceIndex]; 2517 shouldAddResource = types.indexOf(resource.type) > -1; 2518 2519 if (categories && categories.length > 0) { 2520 shouldAddResource = shouldAddResource && categories.indexOf(resource.category) > -1; 2521 } 2522 2523 for (var tagIndex = 0; shouldAddResource && tagIndex < tags.length; tagIndex++) { 2524 tag = tags[tagIndex]; 2525 shouldAddResource = resource.tags.indexOf(tag) > -1; 2526 } 2527 2528 if (shouldAddResource) { 2529 resources.push(resource); 2530 } 2531 } 2532 2533 return resources; 2534 }; 2535 2536 /** 2537 * Close Chip Handler 2538 * @param {Event} event - Click event 2539 * @private 2540 */ 2541 Filter.prototype.closeChipHandler_ = function(event) { 2542 var chip = $(event.currentTarget).parent(); 2543 var checkbox = chip.data('item.dac-filter').find(':first-child')[0]; 2544 checkbox.checked = false; 2545 this.changeStateForCheckbox(checkbox); 2546 }; 2547 2548 /** 2549 * Handle filter item state change. 2550 * @param {Event} event - Change event 2551 * @private 2552 */ 2553 Filter.prototype.toggleCheckboxHandler_ = function(event) { 2554 this.changeStateForCheckbox(event.currentTarget); 2555 }; 2556 2557 /** 2558 * Redraw resource view based on new state. 2559 * @param checkbox 2560 */ 2561 Filter.prototype.changeStateForCheckbox = function(checkbox) { 2562 var item = $(checkbox).parent(); 2563 2564 if (checkbox.checked) { 2565 this.chipsEl_.append(this.chipForItem(item)); 2566 devsite.analytics.trackAnalyticsEvent('event', 2567 'Filters', 'Check', $(checkbox).val()); 2568 } else { 2569 item.data('chip.dac-filter').remove(); 2570 this.addToItemValue(item, -1); 2571 devsite.analytics.trackAnalyticsEvent('event', 2572 'Filters', 'Uncheck', $(checkbox).val()); 2573 } 2574 2575 this.draw_(this.filteredResources()); 2576 }; 2577 2578 /** 2579 * jQuery plugin 2580 */ 2581 $.fn.dacFilter = function() { 2582 return this.each(function() { 2583 var el = $(this); 2584 new Filter(el, el.data()); 2585 }); 2586 }; 2587 2588 /** 2589 * Data Attribute API 2590 */ 2591 $(function() { 2592 $('[data-filter]').dacFilter(); 2593 }); 2594})(jQuery); 2595 2596(function($) { 2597 'use strict'; 2598 2599 /** 2600 * Toggle Floating Label state. 2601 * @param {HTMLElement} el - The DOM element. 2602 * @param options 2603 * @constructor 2604 */ 2605 function FloatingLabel(el, options) { 2606 this.el = $(el); 2607 this.options = $.extend({}, FloatingLabel.DEFAULTS_, options); 2608 this.group = this.el.closest('.dac-form-input-group'); 2609 this.input = this.group.find('.dac-form-input'); 2610 2611 this.checkValue_ = this.checkValue_.bind(this); 2612 this.checkValue_(); 2613 2614 this.input.on('focus', function() { 2615 this.group.addClass('dac-focused'); 2616 }.bind(this)); 2617 this.input.on('blur', function() { 2618 this.group.removeClass('dac-focused'); 2619 this.checkValue_(); 2620 }.bind(this)); 2621 this.input.on('keyup', this.checkValue_); 2622 } 2623 2624 /** 2625 * The label is moved out of the textbox when it has a value. 2626 */ 2627 FloatingLabel.prototype.checkValue_ = function() { 2628 if (this.input.val().length) { 2629 this.group.addClass('dac-has-value'); 2630 } else { 2631 this.group.removeClass('dac-has-value'); 2632 } 2633 }; 2634 2635 /** 2636 * jQuery plugin 2637 * @param {object} options - Override default options. 2638 */ 2639 $.fn.dacFloatingLabel = function(options) { 2640 return this.each(function() { 2641 new FloatingLabel(this, options); 2642 }); 2643 }; 2644 2645 $(document).on('ready.aranja', function() { 2646 $('.dac-form-floatlabel').each(function() { 2647 $(this).dacFloatingLabel($(this).data()); 2648 }); 2649 }); 2650})(jQuery); 2651 2652(function($) { 2653 'use strict'; 2654 2655 /** 2656 * @param {HTMLElement} el - The DOM element. 2657 * @param {Object} options 2658 * @constructor 2659 */ 2660 function Crumbs(selected, options) { 2661 this.options = $.extend({}, Crumbs.DEFAULTS_, options); 2662 this.el = $(this.options.container); 2663 2664 // Do not build breadcrumbs for landing site. 2665 if (!selected || location.pathname === '/index.html' || location.pathname === '/') { 2666 return; 2667 } 2668 2669 // Cache navigation resources 2670 this.selected = $(selected); 2671 this.selectedParent = this.selected.closest('.dac-nav-secondary').siblings('a'); 2672 2673 // Build the breadcrumb list. 2674 this.init(); 2675 } 2676 2677 Crumbs.DEFAULTS_ = { 2678 container: '.dac-header-crumbs', 2679 crumbItem: $('<li class="dac-header-crumbs-item">'), 2680 linkClass: 'dac-header-crumbs-link' 2681 }; 2682 2683 Crumbs.prototype.init = function() { 2684 Crumbs.buildCrumbForLink(this.selected.clone()).appendTo(this.el); 2685 2686 if (this.selectedParent.length) { 2687 Crumbs.buildCrumbForLink(this.selectedParent.clone()).prependTo(this.el); 2688 } 2689 2690 // Reveal the breadcrumbs 2691 this.el.addClass('dac-has-content'); 2692 }; 2693 2694 /** 2695 * Build a HTML structure for a breadcrumb. 2696 * @param {string} link 2697 * @return {jQuery} 2698 */ 2699 Crumbs.buildCrumbForLink = function(link) { 2700 link.find('br').replaceWith(' '); 2701 2702 var crumbLink = $('<a>') 2703 .attr('class', Crumbs.DEFAULTS_.linkClass) 2704 .attr('href', link.attr('href')) 2705 .text(link.text()); 2706 2707 return Crumbs.DEFAULTS_.crumbItem.clone().append(crumbLink); 2708 }; 2709 2710 /** 2711 * jQuery plugin 2712 */ 2713 $.fn.dacCrumbs = function(options) { 2714 return this.each(function() { 2715 new Crumbs(this, options); 2716 }); 2717 }; 2718})(jQuery); 2719 2720(function($) { 2721 'use strict'; 2722 2723 /** 2724 * @param {HTMLElement} el - The DOM element. 2725 * @param {Object} options 2726 * @constructor 2727 */ 2728 function SearchInput(el, options) { 2729 this.el = $(el); 2730 this.options = $.extend({}, SearchInput.DEFAULTS_, options); 2731 this.body = $('body'); 2732 this.input = this.el.find('input'); 2733 this.close = this.el.find(this.options.closeButton); 2734 this.clear = this.el.find(this.options.clearButton); 2735 this.icon = this.el.find('.' + this.options.iconClass); 2736 this.init(); 2737 } 2738 2739 SearchInput.DEFAULTS_ = { 2740 activeClass: 'dac-active', 2741 activeIconClass: 'dac-search', 2742 closeButton: '[data-search-close]', 2743 clearButton: '[data-search-clear]', 2744 hiddenClass: 'dac-hidden', 2745 iconClass: 'dac-header-search-icon', 2746 searchModeClass: 'dac-search-mode', 2747 transitionDuration: 250 2748 }; 2749 2750 SearchInput.prototype.init = function() { 2751 this.input.on('focus.dac-search', this.setActiveState.bind(this)) 2752 .on('input.dac-search', this.checkInputValue.bind(this)); 2753 this.close.on('click.dac-search', this.unsetActiveStateHandler_.bind(this)); 2754 this.clear.on('click.dac-search', this.clearInput.bind(this)); 2755 }; 2756 2757 SearchInput.prototype.setActiveState = function() { 2758 var that = this; 2759 2760 this.clear.addClass(this.options.hiddenClass); 2761 this.body.addClass(this.options.searchModeClass); 2762 this.checkInputValue(); 2763 2764 // Set icon to black after background has faded to white. 2765 setTimeout(function() { 2766 that.icon.addClass(that.options.activeIconClass); 2767 }, this.options.transitionDuration); 2768 }; 2769 2770 SearchInput.prototype.unsetActiveStateHandler_ = function(event) { 2771 event.preventDefault(); 2772 this.unsetActiveState(); 2773 }; 2774 2775 SearchInput.prototype.unsetActiveState = function() { 2776 this.icon.removeClass(this.options.activeIconClass); 2777 this.clear.addClass(this.options.hiddenClass); 2778 this.body.removeClass(this.options.searchModeClass); 2779 }; 2780 2781 SearchInput.prototype.clearInput = function(event) { 2782 event.preventDefault(); 2783 this.input.val(''); 2784 this.clear.addClass(this.options.hiddenClass); 2785 }; 2786 2787 SearchInput.prototype.checkInputValue = function() { 2788 if (this.input.val().length) { 2789 this.clear.removeClass(this.options.hiddenClass); 2790 } else { 2791 this.clear.addClass(this.options.hiddenClass); 2792 } 2793 }; 2794 2795 /** 2796 * jQuery plugin 2797 * @param {object} options - Override default options. 2798 */ 2799 $.fn.dacSearchInput = function() { 2800 return this.each(function() { 2801 var el = $(this); 2802 el.data('search-input.dac', new SearchInput(el, el.data())); 2803 }); 2804 }; 2805 2806 /** 2807 * Data Attribute API 2808 */ 2809 $(function() { 2810 $('[data-search]').dacSearchInput(); 2811 }); 2812})(jQuery); 2813 2814/* global METADATA */ 2815(function($) { 2816 function DacCarouselQuery(el) { 2817 el = $(el); 2818 2819 var opts = el.data(); 2820 opts.maxResults = parseInt(opts.maxResults || '100', 10); 2821 opts.query = opts.carouselQuery; 2822 var resources = window.metadata.query(opts); 2823 2824 el.empty(); 2825 $(resources).each(function() { 2826 var resource = $.extend({}, this, METADATA.carousel[this.url]); 2827 el.dacHero(resource); 2828 }); 2829 2830 // Pagination element. 2831 el.append('<div class="dac-hero-carousel-pagination"><div class="wrap" data-carousel-pagination>'); 2832 2833 el.dacCarousel(); 2834 } 2835 2836 // jQuery plugin 2837 $.fn.dacCarouselQuery = function() { 2838 return this.each(function() { 2839 var el = $(this); 2840 var data = el.data('dac.carouselQuery'); 2841 2842 if (!data) { el.data('dac.carouselQuery', (data = new DacCarouselQuery(el))); } 2843 }); 2844 }; 2845 2846 // Data API 2847 $(function() { 2848 $('[data-carousel-query]').dacCarouselQuery(); 2849 }); 2850})(jQuery); 2851 2852(function($) { 2853 /** 2854 * A CSS based carousel, inspired by SequenceJS. 2855 * @param {jQuery} el 2856 * @param {object} options 2857 * @constructor 2858 */ 2859 function DacCarousel(el, options) { 2860 this.el = $(el); 2861 this.options = options = $.extend({}, DacCarousel.OPTIONS, this.el.data(), options || {}); 2862 this.frames = this.el.find(options.frameSelector); 2863 this.count = this.frames.size(); 2864 this.current = options.start; 2865 2866 this.initPagination(); 2867 this.initEvents(); 2868 this.initFrame(); 2869 } 2870 2871 DacCarousel.OPTIONS = { 2872 auto: true, 2873 autoTime: 10000, 2874 autoMinTime: 5000, 2875 btnPrev: '[data-carousel-prev]', 2876 btnNext: '[data-carousel-next]', 2877 frameSelector: 'article', 2878 loop: true, 2879 start: 0, 2880 swipeThreshold: 160, 2881 pagination: '[data-carousel-pagination]' 2882 }; 2883 2884 DacCarousel.prototype.initPagination = function() { 2885 this.pagination = $([]); 2886 if (!this.options.pagination) { return; } 2887 2888 var pagination = $('<ul class="dac-pagination">'); 2889 var parent = this.el; 2890 if (typeof this.options.pagination === 'string') { parent = this.el.find(this.options.pagination); } 2891 2892 if (this.count > 1) { 2893 for (var i = 0; i < this.count; i++) { 2894 var li = $('<li class="dac-pagination-item">').text(i); 2895 if (i === this.options.start) { li.addClass('active'); } 2896 li.click(this.go.bind(this, i)); 2897 2898 pagination.append(li); 2899 } 2900 this.pagination = pagination.children(); 2901 parent.append(pagination); 2902 } 2903 }; 2904 2905 DacCarousel.prototype.initEvents = function() { 2906 var that = this; 2907 2908 this.touch = { 2909 start: {x: 0, y: 0}, 2910 end: {x: 0, y: 0} 2911 }; 2912 2913 this.el.on('touchstart', this.touchstart_.bind(this)); 2914 this.el.on('touchend', this.touchend_.bind(this)); 2915 this.el.on('touchmove', this.touchmove_.bind(this)); 2916 2917 this.el.hover(function() { 2918 that.pauseRotateTimer(); 2919 }, function() { 2920 that.startRotateTimer(); 2921 }); 2922 2923 $(this.options.btnPrev).click(function(e) { 2924 e.preventDefault(); 2925 that.prev(); 2926 }); 2927 2928 $(this.options.btnNext).click(function(e) { 2929 e.preventDefault(); 2930 that.next(); 2931 }); 2932 }; 2933 2934 DacCarousel.prototype.touchstart_ = function(event) { 2935 var t = event.originalEvent.touches[0]; 2936 this.touch.start = {x: t.screenX, y: t.screenY}; 2937 }; 2938 2939 DacCarousel.prototype.touchend_ = function() { 2940 var deltaX = this.touch.end.x - this.touch.start.x; 2941 var deltaY = Math.abs(this.touch.end.y - this.touch.start.y); 2942 var shouldSwipe = (deltaY < Math.abs(deltaX)) && (Math.abs(deltaX) >= this.options.swipeThreshold); 2943 2944 if (shouldSwipe) { 2945 if (deltaX > 0) { 2946 this.prev(); 2947 } else { 2948 this.next(); 2949 } 2950 } 2951 }; 2952 2953 DacCarousel.prototype.touchmove_ = function(event) { 2954 var t = event.originalEvent.touches[0]; 2955 this.touch.end = {x: t.screenX, y: t.screenY}; 2956 }; 2957 2958 DacCarousel.prototype.initFrame = function() { 2959 this.frames.removeClass('active').eq(this.options.start).addClass('active'); 2960 }; 2961 2962 DacCarousel.prototype.startRotateTimer = function() { 2963 if (!this.options.auto || this.rotateTimer) { return; } 2964 this.rotateTimer = setTimeout(this.next.bind(this), this.options.autoTime); 2965 }; 2966 2967 DacCarousel.prototype.pauseRotateTimer = function() { 2968 clearTimeout(this.rotateTimer); 2969 this.rotateTimer = null; 2970 }; 2971 2972 DacCarousel.prototype.prev = function() { 2973 this.go(this.current - 1); 2974 }; 2975 2976 DacCarousel.prototype.next = function() { 2977 this.go(this.current + 1); 2978 }; 2979 2980 DacCarousel.prototype.go = function(next) { 2981 // Figure out what the next slide is. 2982 while (this.count > 0 && next >= this.count) { next -= this.count; } 2983 while (next < 0) { next += this.count; } 2984 2985 // Cancel if we're already on that slide. 2986 if (next === this.current) { return; } 2987 2988 // Prepare next slide. 2989 this.frames.eq(next).removeClass('out'); 2990 2991 // Recalculate styles before starting slide transition. 2992 this.el.resolveStyles(); 2993 // Update pagination 2994 this.pagination.removeClass('active').eq(next).addClass('active'); 2995 2996 // Transition out current frame 2997 this.frames.eq(this.current).toggleClass('active out'); 2998 2999 // Transition in a new frame 3000 this.frames.eq(next).toggleClass('active'); 3001 3002 this.current = next; 3003 }; 3004 3005 // Helper which resolves new styles for an element, so it can start transitioning 3006 // from the new values. 3007 $.fn.resolveStyles = function() { 3008 /*jshint expr:true*/ 3009 this[0] && this[0].offsetTop; 3010 return this; 3011 }; 3012 3013 // jQuery plugin 3014 $.fn.dacCarousel = function() { 3015 this.each(function() { 3016 var $el = $(this); 3017 $el.data('dac-carousel', new DacCarousel(this)); 3018 }); 3019 return this; 3020 }; 3021 3022 // Data API 3023 $(function() { 3024 $('[data-carousel]').dacCarousel(); 3025 }); 3026})(jQuery); 3027 3028/* global toRoot */ 3029 3030(function($) { 3031 // Ordering matters 3032 var TAG_MAP = [ 3033 {from: 'developerstory', to: 'Android Developer Story'}, 3034 {from: 'googleplay', to: 'Google Play'} 3035 ]; 3036 3037 function DacHero(el, resource, isSearch) { 3038 var slide = $('<article>'); 3039 slide.addClass(isSearch ? 'dac-search-hero' : 'dac-expand dac-hero'); 3040 var image = cleanUrl(resource.heroImage || resource.image); 3041 var fullBleed = image && !resource.heroColor; 3042 3043 if (!isSearch) { 3044 // Configure background 3045 slide.css({ 3046 backgroundImage: fullBleed ? 'url(' + image + ')' : '', 3047 backgroundColor: resource.heroColor || '' 3048 }); 3049 3050 // Should copy be inverted 3051 slide.toggleClass('dac-invert', resource.heroInvert || fullBleed); 3052 slide.toggleClass('dac-darken', fullBleed); 3053 3054 // Should be clickable 3055 slide.append($('<a class="dac-hero-carousel-action">').attr('href', cleanUrl(resource.url))); 3056 } 3057 3058 var cols = $('<div class="cols dac-hero-content">'); 3059 3060 // inline image column 3061 var rightCol = $('<div class="col-1of2 col-push-1of2 dac-hero-figure">') 3062 .appendTo(cols); 3063 3064 if ((!fullBleed || isSearch) && image) { 3065 rightCol.append($('<img>').attr('src', image)); 3066 } 3067 3068 // info column 3069 $('<div class="col-1of2 col-pull-1of2">') 3070 .append($('<div class="dac-hero-tag">').text(formatTag(resource))) 3071 .append($('<h1 class="dac-hero-title">').text(formatTitle(resource))) 3072 .append($('<p class="dac-hero-description">').text(resource.summary)) 3073 .append($('<a class="dac-hero-cta">') 3074 .text(formatCTA(resource)) 3075 .attr('href', cleanUrl(resource.url)) 3076 .prepend($('<span class="dac-sprite dac-auto-chevron">')) 3077 ) 3078 .appendTo(cols); 3079 3080 slide.append(cols.wrap('<div class="wrap">').parent()); 3081 el.append(slide); 3082 } 3083 3084 function cleanUrl(url) { 3085 if (url && url.indexOf('//') === -1) { 3086 url = toRoot + url; 3087 } 3088 return url; 3089 } 3090 3091 function formatTag(resource) { 3092 // Hmm, need a better more scalable solution for this. 3093 for (var i = 0, mapping; mapping = TAG_MAP[i]; i++) { 3094 if (resource.tags.indexOf(mapping.from) > -1) { 3095 return mapping.to; 3096 } 3097 } 3098 return resource.type; 3099 } 3100 3101 function formatTitle(resource) { 3102 return resource.title.replace(/android developer story: /i, ''); 3103 } 3104 3105 function formatCTA(resource) { 3106 return resource.type === 'youtube' ? 'Watch the video' : 'Learn more'; 3107 } 3108 3109 // jQuery plugin 3110 $.fn.dacHero = function(resource, isSearch) { 3111 return this.each(function() { 3112 var el = $(this); 3113 return new DacHero(el, resource, isSearch); 3114 }); 3115 }; 3116})(jQuery); 3117 3118(function($) { 3119 'use strict'; 3120 3121 function highlightString(label, query) { 3122 query = query || ''; 3123 //query = query.replace('<wbr>', '').replace('.', '\\.'); 3124 var queryRE = new RegExp('(' + query + ')', 'ig'); 3125 return label.replace(queryRE, '<em>$1</em>'); 3126 } 3127 3128 $.fn.highlightMatches = function(query) { 3129 return this.each(function() { 3130 var el = $(this); 3131 var label = el.html(); 3132 var highlighted = highlightString(label, query); 3133 el.html(highlighted); 3134 el.addClass('highlighted'); 3135 }); 3136 }; 3137})(jQuery); 3138 3139/** 3140 * History tracking. 3141 * Track visited urls in localStorage. 3142 */ 3143(function($) { 3144 var PAGES_TO_STORE_ = 100; 3145 var MIN_NUMBER_OF_PAGES_TO_DISPLAY_ = 6; 3146 var CONTAINER_SELECTOR_ = '.dac-search-results-history-wrap'; 3147 3148 /** 3149 * Generate resource cards for visited pages. 3150 * @param {HTMLElement} el 3151 * @constructor 3152 */ 3153 function HistoryQuery(el) { 3154 this.el = $(el); 3155 3156 // Only show history component if enough pages have been visited. 3157 if (getVisitedPages().length < MIN_NUMBER_OF_PAGES_TO_DISPLAY_) { 3158 this.el.closest(CONTAINER_SELECTOR_).addClass('dac-hidden'); 3159 return; 3160 } 3161 3162 // Rename query 3163 this.el.data('query', this.el.data('history-query')); 3164 3165 // jQuery method to populate cards. 3166 this.el.resourceWidget(); 3167 } 3168 3169 /** 3170 * Fetch from localStorage an array of visted pages 3171 * @returns {Array} 3172 */ 3173 function getVisitedPages() { 3174 var visited = localStorage.getItem('visited-pages'); 3175 return visited ? JSON.parse(visited) : []; 3176 } 3177 3178 /** 3179 * Return a page corresponding to cuurent pathname. If none exists, create one. 3180 * @param {Array} pages 3181 * @param {String} path 3182 * @returns {Object} Page 3183 */ 3184 function getPageForPath(pages, path) { 3185 var page; 3186 3187 // Backwards lookup for current page, last pages most likely to be visited again. 3188 for (var i = pages.length - 1; i >= 0; i--) { 3189 if (pages[i].path === path) { 3190 page = pages[i]; 3191 3192 // Remove page object from pages list to ensure correct ordering. 3193 pages.splice(i, 1); 3194 3195 return page; 3196 } 3197 } 3198 3199 // If storage limit is exceeded, remove last visited path. 3200 if (pages.length >= PAGES_TO_STORE_) { 3201 pages.shift(); 3202 } 3203 3204 return {path: path}; 3205 } 3206 3207 /** 3208 * Add current page to back of visited array, increase hit count by 1. 3209 */ 3210 function addCurrectPage() { 3211 var path = location.pathname; 3212 3213 // Do not track frontpage visits. 3214 if (path === '/' || path === '/index.html') {return;} 3215 3216 var pages = getVisitedPages(); 3217 var page = getPageForPath(pages, path); 3218 3219 // New page visits have no hit count. 3220 page.hit = ~~page.hit + 1; 3221 3222 // Most recently visted pages are located at the end of the visited array. 3223 pages.push(page); 3224 3225 localStorage.setItem('visited-pages', JSON.stringify(pages)); 3226 } 3227 3228 /** 3229 * Hit count compare function. 3230 * @param {Object} a - page 3231 * @param {Object} b - page 3232 * @returns {number} 3233 */ 3234 function byHit(a, b) { 3235 if (a.hit > b.hit) { 3236 return -1; 3237 } else if (a.hit < b.hit) { 3238 return 1; 3239 } 3240 3241 return 0; 3242 } 3243 3244 /** 3245 * Return a list of visited urls in a given order. 3246 * @param {String} order - (recent|most-visited) 3247 * @returns {Array} 3248 */ 3249 $.dacGetVisitedUrls = function(order) { 3250 var pages = getVisitedPages(); 3251 3252 if (order === 'recent') { 3253 pages.reverse(); 3254 } else { 3255 pages.sort(byHit); 3256 } 3257 3258 return pages.map(function(page) { 3259 return page.path.replace(/^\//, ''); 3260 }); 3261 }; 3262 3263 // jQuery plugin 3264 $.fn.dacHistoryQuery = function() { 3265 return this.each(function() { 3266 var el = $(this); 3267 var data = el.data('dac.recentlyVisited'); 3268 3269 if (!data) { 3270 el.data('dac.recentlyVisited', (data = new HistoryQuery(el))); 3271 } 3272 }); 3273 }; 3274 3275 $(function() { 3276 $('[data-history-query]').dacHistoryQuery(); 3277 // Do not block page rendering. 3278 setTimeout(addCurrectPage, 0); 3279 }); 3280})(jQuery); 3281 3282/* ############################################ */ 3283/* ########## LOCALIZATION ############ */ 3284/* ############################################ */ 3285/** 3286 * Global helpers. 3287 */ 3288function getBaseUri(uri) { 3289 var intlUrl = (uri.substring(0, 6) === '/intl/'); 3290 if (intlUrl) { 3291 var base = uri.substring(uri.indexOf('intl/') + 5, uri.length); 3292 base = base.substring(base.indexOf('/') + 1, base.length); 3293 return '/' + base; 3294 } else { 3295 return uri; 3296 } 3297} 3298 3299function changeLangPref(targetLang, submit) { 3300 window.writeCookie('pref_lang', targetLang, null); 3301 $('#language').find('option[value="' + targetLang + '"]').attr('selected', true); 3302 if (submit) { 3303 $('#setlang').submit(); 3304 } 3305} 3306// Redundant usage to appease jshint. 3307window.changeLangPref = changeLangPref; 3308 3309(function() { 3310 /** 3311 * Whitelisted locales. Should match choices in language dropdown. Repeated here 3312 * as a lot of i18n logic happens before page load and dropdown is ready. 3313 */ 3314 var LANGUAGES = [ 3315 'en', 3316 'es', 3317 'id', 3318 'ja', 3319 'ko', 3320 'pt-br', 3321 'ru', 3322 'vi', 3323 'zh-cn', 3324 'zh-tw' 3325 ]; 3326 3327 /** 3328 * Master list of translated strings for template files. 3329 */ 3330 var PHRASES = { 3331 'newsletter': { 3332 'title': 'Get the latest Android developer news and tips that will help you find success on Google Play.', 3333 'requiredHint': '* Required Fields', 3334 'name': 'Full name', 3335 'email': 'Email address', 3336 'company': 'Company / developer name', 3337 'appUrl': 'One of your Play Store app URLs', 3338 'business': { 3339 'label': 'Which best describes your business:', 3340 'apps': 'Apps', 3341 'games': 'Games', 3342 'both': 'Apps & Games' 3343 }, 3344 'confirmMailingList': 'Add me to the mailing list for the monthly newsletter and occasional emails about ' + 3345 'development and Google Play opportunities.', 3346 'privacyPolicy': 'I acknowledge that the information provided in this form will be subject to Google\'s ' + 3347 '<a href="https://www.google.com/policies/privacy/" target="_blank">privacy policy</a>.', 3348 'languageVal': 'English', 3349 'successTitle': 'Hooray!', 3350 'successDetails': 'You have successfully signed up for the latest Android developer news and tips.', 3351 'languageValTarget': { 3352 'en': 'English', 3353 'ar': 'Arabic (العربيّة)', 3354 'id': 'Indonesian (Bahasa)', 3355 'fr': 'French (français)', 3356 'de': 'German (Deutsch)', 3357 'ja': 'Japanese (日本語)', 3358 'ko': 'Korean (한국어)', 3359 'ru': 'Russian (Русский)', 3360 'es': 'Spanish (español)', 3361 'th': 'Thai (ภาษาไทย)', 3362 'tr': 'Turkish (Türkçe)', 3363 'vi': 'Vietnamese (tiếng Việt)', 3364 'pt-br': 'Brazilian Portuguese (Português Brasileiro)', 3365 'zh-cn': 'Simplified Chinese (简体中文)', 3366 'zh-tw': 'Traditional Chinese (繁體中文)', 3367 }, 3368 'resetLangTitle': "Browse this site in %{targetLang}?", 3369 'resetLangTextIntro': 'You requested a page in %{targetLang}, but your language preference for this site is %{lang}.', 3370 'resetLangTextCta': 'Would you like to change your language preference and browse this site in %{targetLang}? ' + 3371 'If you want to change your language preference later, use the language menu at the bottom of each page.', 3372 'resetLangButtonYes': 'Change Language', 3373 'resetLangButtonNo': 'Not Now' 3374 } 3375 }; 3376 3377 /** 3378 * Current locale. 3379 */ 3380 var locale = (function() { 3381 var lang = window.readCookie('pref_lang'); 3382 if (lang === 0 || LANGUAGES.indexOf(lang) === -1) { 3383 lang = 'en'; 3384 } 3385 return lang; 3386 })(); 3387 var localeTarget = (function() { 3388 var lang = getQueryVariable('hl'); 3389 if (lang === false || LANGUAGES.indexOf(lang) === -1) { 3390 lang = locale; 3391 } 3392 return lang; 3393 })(); 3394 3395 /** 3396 * Global function shims for backwards compatibility 3397 */ 3398 window.changeNavLang = function() { 3399 // Already done. 3400 }; 3401 3402 window.loadLangPref = function() { 3403 // Languages pref already loaded. 3404 }; 3405 3406 window.getLangPref = function() { 3407 return locale; 3408 }; 3409 3410 window.getLangTarget = function() { 3411 return localeTarget; 3412 }; 3413 3414 // Expose polyglot instance for advanced localization. 3415 var polyglot = window.polyglot = new window.Polyglot({ 3416 locale: locale, 3417 phrases: PHRASES 3418 }); 3419 3420 // When DOM is ready. 3421 $(function() { 3422 // Mark current locale in language picker. 3423 $('#language').find('option[value="' + locale + '"]').attr('selected', true); 3424 3425 $('html').dacTranslate().on('dac:domchange', function(e) { 3426 $(e.target).dacTranslate(); 3427 }); 3428 }); 3429 3430 $.fn.dacTranslate = function() { 3431 // Translate strings in template markup: 3432 3433 // OLD 3434 // Having all translations in HTML does not scale well and bloats every page. 3435 // Need to migrate this to data-l JS translations below. 3436 if (locale !== 'en') { 3437 var $links = this.find('a[' + locale + '-lang]'); 3438 $links.each(function() { // for each link with a translation 3439 var $link = $(this); 3440 // put the desired language from the attribute as the text 3441 $link.text($link.attr(locale + '-lang')); 3442 }); 3443 } 3444 3445 // NEW 3446 // A simple declarative api for JS translations. Feel free to extend as appropriate. 3447 3448 // Miscellaneous string compilations 3449 // Build full strings from localized substrings: 3450 var myLocaleTarget = window.getLangTarget(); 3451 var myTargetLang = window.polyglot.t("newsletter.languageValTarget." + myLocaleTarget); 3452 var myLang = window.polyglot.t("newsletter.languageVal"); 3453 var myTargetLangTitleString = window.polyglot.t("newsletter.resetLangTitle", {targetLang: myTargetLang}); 3454 var myResetLangTextIntro = window.polyglot.t("newsletter.resetLangTextIntro", {targetLang: myTargetLang, lang: myLang}); 3455 var myResetLangTextCta = window.polyglot.t("newsletter.resetLangTextCta", {targetLang: myTargetLang}); 3456 //var myResetLangButtonYes = window.polyglot.t("newsletter.resetLangButtonYes", {targetLang: myTargetLang}); 3457 3458 // Inject strings as text values in dialog components: 3459 $("#langform .dac-modal-header-title").text(myTargetLangTitleString); 3460 $("#langform #resetLangText").text(myResetLangTextIntro); 3461 $("#langform #resetLangCta").text(myResetLangTextCta); 3462 //$("#resetLangButtonYes").attr("data-t", window.polyglot.t(myResetLangButtonYes)); 3463 3464 // Text: <div data-t="nav.home"></div> 3465 // HTML: <div data-t="privacy" data-t-html></html> 3466 this.find('[data-t]').each(function() { 3467 var el = $(this); 3468 var data = el.data(); 3469 if (data.t) { 3470 el[data.tHtml === '' ? 'html' : 'text'](polyglot.t(data.t)); 3471 } 3472 }); 3473 3474 return this; 3475 }; 3476})(); 3477/* ########## END LOCALIZATION ############ */ 3478 3479// Translations. These should eventually be moved into language-specific files and loaded on demand. 3480// jshint nonbsp:false 3481switch (window.getLangPref()) { 3482 case 'ar': 3483 window.polyglot.extend({ 3484 'newsletter': { 3485 'title': 'Google Play. يمكنك الحصول على آخر الأخبار والنصائح من مطوّري تطبيقات Android، مما يساعدك ' + 3486 'على تحقيق النجاح على', 3487 'requiredHint': '* حقول مطلوبة', 3488 'name': '. الاسم بالكامل ', 3489 'email': '. عنوان البريد الإلكتروني ', 3490 'company': '. اسم الشركة / اسم مطوّر البرامج', 3491 'appUrl': '. أحد عناوين URL لتطبيقاتك في متجر Play', 3492 'business': { 3493 'label': '. ما العنصر الذي يوضح طبيعة نشاطك التجاري بدقة؟ ', 3494 'apps': 'التطبيقات', 3495 'games': 'الألعاب', 3496 'both': 'التطبيقات والألعاب' 3497 }, 3498 'confirmMailingList': 'إضافتي إلى القائمة البريدية للنشرة الإخبارية الشهرية والرسائل الإلكترونية التي يتم' + 3499 ' إرسالها من حين لآخر بشأن التطوير وفرص Google Play.', 3500 'privacyPolicy': 'أقر بأن المعلومات المقدَّمة في هذا النموذج تخضع لسياسة خصوصية ' + 3501 '<a href="https://www.google.com/intl/ar/policies/privacy/" target="_blank">Google</a>.', 3502 'languageVal': 'Arabic (العربيّة)', 3503 'successTitle': 'رائع!', 3504 'successDetails': 'لقد اشتركت بنجاح للحصول على آخر الأخبار والنصائح من مطوّري برامج Android.' 3505 } 3506 }); 3507 break; 3508 case 'zh-cn': 3509 window.polyglot.extend({ 3510 'newsletter': { 3511 'title': '获取最新的 Android 开发者资讯和提示,助您在 Google Play 上取得成功。', 3512 'requiredHint': '* 必填字段', 3513 'name': '全名', 3514 'email': '电子邮件地址', 3515 'company': '公司/开发者名称', 3516 'appUrl': '您的某个 Play 商店应用网址', 3517 'business': { 3518 'label': '哪一项能够最准确地描述您的业务?', 3519 'apps': '应用', 3520 'games': '游戏', 3521 'both': '应用和游戏' 3522 }, 3523 'confirmMailingList': '将我添加到邮寄名单,以便接收每月简报以及不定期发送的关于开发和 Google Play 商机的电子邮件。', 3524 'privacyPolicy': '我确认自己了解在此表单中提供的信息受 <a href="https://www.google.com/intl/zh-CN/' + 3525 'policies/privacy/" target="_blank">Google</a> 隐私权政策的约束。', 3526 'languageVal': 'Simplified Chinese (简体中文)', 3527 'successTitle': '太棒了!', 3528 'successDetails': '您已成功订阅最新的 Android 开发者资讯和提示。' 3529 } 3530 }); 3531 break; 3532 case 'zh-tw': 3533 window.polyglot.extend({ 3534 'newsletter': { 3535 'title': '獲得 Android 開發人員的最新消息和各項秘訣,讓您在 Google Play 上輕鬆邁向成功之路。', 3536 'requiredHint': '* 必要欄位', 3537 'name': '全名', 3538 'email': '電子郵件地址', 3539 'company': '公司/開發人員名稱', 3540 'appUrl': '您其中一個 Play 商店應用程式的網址', 3541 'business': { 3542 'label': '為您的商家選取最合適的產品類別。', 3543 'apps': '應用程式', 3544 'games': '遊戲', 3545 'both': '應用程式和遊戲' 3546 }, 3547 'confirmMailingList': '我想加入 Google Play 的郵寄清單,以便接收每月電子報和 Google Play 不定期寄送的電子郵件,' + 3548 '瞭解關於開發和 Google Play 商機的資訊。', 3549 'privacyPolicy': '我瞭解,我在這張表單中提供的資訊將受到 <a href="' + 3550 'https://www.google.com/intl/zh-TW/policies/privacy/" target="_blank">Google</a> 隱私權政策.', 3551 'languageVal': 'Traditional Chinese (繁體中文)', 3552 'successTitle': '太棒了!', 3553 'successDetails': '您已經成功訂閱 Android 開發人員的最新消息和各項秘訣。' 3554 } 3555 }); 3556 break; 3557 case 'fr': 3558 window.polyglot.extend({ 3559 'newsletter': { 3560 'title': 'Recevez les dernières actualités destinées aux développeurs Android, ainsi que des conseils qui ' + 3561 'vous mèneront vers le succès sur Google Play.', 3562 'requiredHint': '* Champs obligatoires', 3563 'name': 'Nom complet', 3564 'email': 'Adresse e-mail', 3565 'company': 'Nom de la société ou du développeur', 3566 'appUrl': 'Une de vos URL Play Store', 3567 'business': { 3568 'label': 'Quelle option décrit le mieux votre activité ?', 3569 'apps': 'Applications', 3570 'games': 'Jeux', 3571 'both': 'Applications et jeux' 3572 }, 3573 'confirmMailingList': 'Ajoutez-moi à la liste de diffusion de la newsletter mensuelle et tenez-moi informé ' + 3574 'par des e-mails occasionnels de l\'évolution et des opportunités de Google Play.', 3575 'privacyPolicy': 'Je comprends que les renseignements fournis dans ce formulaire seront soumis aux <a href="' + 3576 'https://www.google.com/intl/fr/policies/privacy/" target="_blank">règles de confidentialité</a> de Google.', 3577 'languageVal': 'French (français)', 3578 'successTitle': 'Super !', 3579 'successDetails': 'Vous êtes bien inscrit pour recevoir les actualités et les conseils destinés aux ' + 3580 'développeurs Android.' 3581 } 3582 }); 3583 break; 3584 case 'de': 3585 window.polyglot.extend({ 3586 'newsletter': { 3587 'title': 'Abonniere aktuelle Informationen und Tipps für Android-Entwickler und werde noch erfolgreicher ' + 3588 'bei Google Play.', 3589 'requiredHint': '* Pflichtfelder', 3590 'name': 'Vollständiger Name', 3591 'email': 'E-Mail-Adresse', 3592 'company': 'Unternehmens-/Entwicklername', 3593 'appUrl': 'Eine der URLs deiner Play Store App', 3594 'business': { 3595 'label': 'Welche der folgenden Kategorien beschreibt dein Unternehmen am besten?', 3596 'apps': 'Apps', 3597 'games': 'Spiele', 3598 'both': 'Apps und Spiele' 3599 }, 3600 'confirmMailingList': 'Meine E-Mail-Adresse soll zur Mailingliste hinzugefügt werden, damit ich den ' + 3601 'monatlichen Newsletter sowie gelegentlich E-Mails zu Entwicklungen und Optionen bei Google Play erhalte.', 3602 'privacyPolicy': 'Ich bestätige, dass die in diesem Formular bereitgestellten Informationen gemäß der ' + 3603 '<a href="https://www.google.com/intl/de/policies/privacy/" target="_blank">Datenschutzerklärung</a> von ' + 3604 'Google verwendet werden dürfen.', 3605 'languageVal': 'German (Deutsch)', 3606 'successTitle': 'Super!', 3607 'successDetails': 'Du hast dich erfolgreich angemeldet und erhältst jetzt aktuelle Informationen und Tipps ' + 3608 'für Android-Entwickler.' 3609 } 3610 }); 3611 break; 3612 case 'id': 3613 window.polyglot.extend({ 3614 'newsletter': { 3615 'title': 'Receba as dicas e as notícias mais recentes para os desenvolvedores Android e seja bem-sucedido ' + 3616 'no Google Play.', 3617 'requiredHint': '* Bidang Wajib Diisi', 3618 'name': 'Nama lengkap', 3619 'email': 'Alamat email', 3620 'company': 'Nama pengembang / perusahaan', 3621 'appUrl': 'Salah satu URL aplikasi Play Store Anda', 3622 'business': { 3623 'label': 'Dari berikut ini, mana yang paling cocok dengan bisnis Anda?', 3624 'apps': 'Aplikasi', 3625 'games': 'Game', 3626 'both': 'Aplikasi dan Game' 3627 }, 3628 'confirmMailingList': 'Tambahkan saya ke milis untuk mendapatkan buletin bulanan dan email sesekali mengenai ' + 3629 'perkembangan dan kesempatan yang ada di Google Play.', 3630 'privacyPolicy': 'Saya memahami bahwa informasi yang diberikan dalam formulir ini tunduk pada <a href="' + 3631 'https://www.google.com/intl/in/policies/privacy/" target="_blank">kebijakan privasi</a> Google.', 3632 'languageVal': 'Indonesian (Bahasa)', 3633 'successTitle': 'Hore!', 3634 'successDetails': 'Anda berhasil mendaftar untuk kiat dan berita pengembang Android terbaru.' 3635 } 3636 }); 3637 break; 3638 case 'it': 3639 //window.polyglot.extend({ 3640 // 'newsletter': { 3641 // 'title': 'Receba as dicas e as notícias mais recentes para os desenvolvedores Android e seja bem-sucedido ' + 3642 // 'no Google Play.', 3643 // 'requiredHint': '* Campos obrigatórios', 3644 // 'name': 'Nome completo', 3645 // 'email': 'Endereço de Email', 3646 // 'company': 'Nome da empresa / do desenvolvedor', 3647 // 'appUrl': 'URL de um dos seus apps da Play Store', 3648 // 'business': { 3649 // 'label': 'Qual das seguintes opções melhor descreve sua empresa?', 3650 // 'apps': 'Apps', 3651 // 'games': 'Jogos', 3652 // 'both': 'Apps e Jogos' 3653 // }, 3654 // 'confirmMailingList': 'Inscreva-me na lista de e-mails para que eu receba o boletim informativo mensal, ' + 3655 // 'bem como e-mails ocasionais sobre o desenvolvimento e as oportunidades do Google Play.', 3656 // 'privacyPolicy': 'Reconheço que as informações fornecidas neste formulário estão sujeitas à <a href="' + 3657 // 'https://www.google.com.br/policies/privacy/" target="_blank">Política de Privacidade</a> do Google.', 3658 // 'languageVal': 'Italian (italiano)', 3659 // 'successTitle': 'Uhu!', 3660 // 'successDetails': 'Você se inscreveu para receber as notícias e as dicas mais recentes para os ' + 3661 // 'desenvolvedores Android.', 3662 // } 3663 //}); 3664 break; 3665 case 'ja': 3666 window.polyglot.extend({ 3667 'newsletter': { 3668 'title': 'Google Play での成功に役立つ Android デベロッパー向けの最新ニュースやおすすめの情報をお届けします。', 3669 'requiredHint': '* 必須', 3670 'name': '氏名', 3671 'email': 'メールアドレス', 3672 'company': '会社名 / デベロッパー名', 3673 'appUrl': 'Play ストア アプリの URL(いずれか 1 つ)', 3674 'business': { 3675 'label': 'お客様のビジネスに最もよく当てはまるものをお選びください。', 3676 'apps': 'アプリ', 3677 'games': 'ゲーム', 3678 'both': 'アプリとゲーム' 3679 }, 3680 'confirmMailingList': '開発や Google Play の最新情報に関する毎月発行のニュースレターや不定期発行のメールを受け取る', 3681 'privacyPolicy': 'このフォームに入力した情報に <a href="https://www.google.com/intl/ja/policies/privacy/" ' + 3682 'target="_blank">Google</a> のプライバシー ポリシーが適用', 3683 'languageVal': 'Japanese (日本語)', 3684 'successTitle': '完了です!', 3685 'successDetails': 'Android デベロッパー向けの最新ニュースやおすすめの情報の配信登録が完了しました。' 3686 } 3687 }); 3688 break; 3689 case 'ko': 3690 window.polyglot.extend({ 3691 'newsletter': { 3692 'title': 'Google Play에서 성공을 거두는 데 도움이 되는 최신 Android 개발자 소식 및 도움말을 받아 보세요.', 3693 'requiredHint': '* 필수 입력란', 3694 'name': '이름', 3695 'email': '이메일 주소', 3696 'company': '회사/개발자 이름', 3697 'appUrl': 'Play 스토어 앱 URL 중 1개', 3698 'business': { 3699 'label': '다음 중 내 비즈니스를 가장 잘 설명하는 단어는 무엇인가요?', 3700 'apps': '앱', 3701 'games': '게임', 3702 'both': '앱 및 게임' 3703 }, 3704 'confirmMailingList': '개발 및 Google Play 관련 소식에 관한 월별 뉴스레터 및 비정기 이메일을 받아보겠습니다.', 3705 'privacyPolicy': '이 양식에 제공한 정보는 <a href="https://www.google.com/intl/ko/policies/privacy/" ' + 3706 'target="_blank">Google의</a> 개인정보취급방침에 따라 사용됨을', 3707 'languageVal':'Korean (한국어)', 3708 'successTitle': '축하합니다!', 3709 'successDetails': '최신 Android 개발자 뉴스 및 도움말을 받아볼 수 있도록 가입을 완료했습니다.' 3710 } 3711 }); 3712 break; 3713 case 'pt-br': 3714 window.polyglot.extend({ 3715 'newsletter': { 3716 'title': 'Receba as dicas e as notícias mais recentes para os desenvolvedores Android e seja bem-sucedido ' + 3717 'no Google Play.', 3718 'requiredHint': '* Campos obrigatórios', 3719 'name': 'Nome completo', 3720 'email': 'Endereço de Email', 3721 'company': 'Nome da empresa / do desenvolvedor', 3722 'appUrl': 'URL de um dos seus apps da Play Store', 3723 'business': { 3724 'label': 'Qual das seguintes opções melhor descreve sua empresa?', 3725 'apps': 'Apps', 3726 'games': 'Jogos', 3727 'both': 'Apps e Jogos' 3728 }, 3729 'confirmMailingList': 'Inscreva-me na lista de e-mails para que eu receba o boletim informativo mensal, ' + 3730 'bem como e-mails ocasionais sobre o desenvolvimento e as oportunidades do Google Play.', 3731 'privacyPolicy': 'Reconheço que as informações fornecidas neste formulário estão sujeitas à <a href="' + 3732 'https://www.google.com.br/policies/privacy/" target="_blank">Política de Privacidade</a> do Google.', 3733 'languageVal': 'Brazilian Portuguese (Português Brasileiro)', 3734 'successTitle': 'Uhu!', 3735 'successDetails': 'Você se inscreveu para receber as notícias e as dicas mais recentes para os ' + 3736 'desenvolvedores Android.' 3737 } 3738 }); 3739 break; 3740 case 'ru': 3741 window.polyglot.extend({ 3742 'newsletter': { 3743 'title': 'Хотите получать последние новости и советы для разработчиков Google Play? Заполните эту форму.', 3744 'requiredHint': '* Обязательные поля', 3745 'name': 'Полное имя', 3746 'email': 'Адрес электронной почты', 3747 'company': 'Название компании или имя разработчика', 3748 'appUrl': 'Ссылка на любое ваше приложение в Google Play', 3749 'business': { 3750 'label': 'Что вы создаете?', 3751 'apps': 'Приложения', 3752 'games': 'Игры', 3753 'both': 'Игры и приложения' 3754 }, 3755 'confirmMailingList': 'Я хочу получать ежемесячную рассылку для разработчиков и другие полезные новости ' + 3756 'Google Play.', 3757 'privacyPolicy': 'Я предоставляю эти данные в соответствии с <a href="' + 3758 'https://www.google.com/intl/ru/policies/privacy/" target="_blank">Политикой конфиденциальности</a> Google.', 3759 'languageVal': 'Russian (Русский)', 3760 'successTitle': 'Поздравляем!', 3761 'successDetails': 'Теперь вы подписаны на последние новости и советы для разработчиков Android.' 3762 } 3763 }); 3764 break; 3765 case 'es': 3766 window.polyglot.extend({ 3767 'newsletter': { 3768 'title': 'Recibe las últimas noticias y sugerencias para programadores de Android y logra tener éxito en ' + 3769 'Google Play.', 3770 'requiredHint': '* Campos obligatorios', 3771 'name': 'Dirección de correo electrónico', 3772 'email': 'Endereço de Email', 3773 'company': 'Nombre de la empresa o del programador', 3774 'appUrl': 'URL de una de tus aplicaciones de Play Store', 3775 'business': { 3776 'label': '¿Qué describe mejor a tu empresa?', 3777 'apps': 'Aplicaciones', 3778 'games': 'Juegos', 3779 'both': 'Juegos y aplicaciones' 3780 }, 3781 'confirmMailingList': 'Deseo unirme a la lista de distribución para recibir el boletín informativo mensual ' + 3782 'y correos electrónicos ocasionales sobre desarrollo y oportunidades de Google Play.', 3783 'privacyPolicy': 'Acepto que la información que proporcioné en este formulario cumple con la <a href="' + 3784 'https://www.google.com/intl/es/policies/privacy/" target="_blank">política de privacidad</a> de Google.', 3785 'languageVal': 'Spanish (español)', 3786 'successTitle': '¡Felicitaciones!', 3787 'successDetails': 'El registro para recibir las últimas noticias y sugerencias para programadores de Android ' + 3788 'se realizó correctamente.' 3789 } 3790 }); 3791 break; 3792 case 'th': 3793 window.polyglot.extend({ 3794 'newsletter': { 3795 'title': 'รับข่าวสารล่าสุดสำหรับนักพัฒนาซอฟต์แวร์ Android ตลอดจนเคล็ดลับที่จะช่วยให้คุณประสบความสำเร็จบน ' + 3796 'Google Play', 3797 'requiredHint': '* ช่องที่ต้องกรอก', 3798 'name': 'ชื่อและนามสกุล', 3799 'email': 'ที่อยู่อีเมล', 3800 'company': 'ชื่อบริษัท/นักพัฒนาซอฟต์แวร์', 3801 'appUrl': 'URL แอปใดแอปหนึ่งของคุณใน Play สโตร์', 3802 'business': { 3803 'label': 'ข้อใดตรงกับธุรกิจของคุณมากที่สุด', 3804 'apps': 'แอป', 3805 'games': 'เกม', 3806 'both': 'แอปและเกม' 3807 }, 3808 'confirmMailingList': 'เพิ่มฉันลงในรายชื่ออีเมลเพื่อรับจดหมายข่าวรายเดือนและอีเมลเป็นครั้งคราวเกี่ยวกับก' + 3809 'ารพัฒนาซอฟต์แวร์และโอกาสใน Google Play', 3810 'privacyPolicy': 'ฉันรับทราบว่าข้อมูลที่ให้ไว้ในแบบฟอร์มนี้จะเป็นไปตามนโยบายส่วนบุคคลของ ' + 3811 '<a href="https://www.google.com/intl/th/policies/privacy/" target="_blank">Google</a>', 3812 'languageVal': 'Thai (ภาษาไทย)', 3813 'successTitle': 'ไชโย!', 3814 'successDetails': 'คุณลงชื่อสมัครรับข่าวสารและเคล็ดลับล่าสุดสำหรับนักพัฒนาซอฟต์แวร์ Android เสร็จเรียบร้อยแล้ว' 3815 } 3816 }); 3817 break; 3818 case 'tr': 3819 window.polyglot.extend({ 3820 'newsletter': { 3821 'title': 'Google Play\'de başarılı olmanıza yardımcı olacak en son Android geliştirici haberleri ve ipuçları.', 3822 'requiredHint': '* Zorunlu Alanlar', 3823 'name': 'Tam ad', 3824 'email': 'E-posta adresi', 3825 'company': 'Şirket / geliştirici adı', 3826 'appUrl': 'Play Store uygulama URL\'lerinizden biri', 3827 'business': { 3828 'label': 'İşletmenizi en iyi hangisi tanımlar?', 3829 'apps': 'Uygulamalar', 3830 'games': 'Oyunlar', 3831 'both': 'Uygulamalar ve Oyunlar' 3832 }, 3833 'confirmMailingList': 'Beni, geliştirme ve Google Play fırsatlarıyla ilgili ara sıra gönderilen e-posta ' + 3834 'iletilerine ilişkin posta listesine ve aylık haber bültenine ekle.', 3835 'privacyPolicy': 'Bu formda sağlanan bilgilerin Google\'ın ' + 3836 '<a href="https://www.google.com/intl/tr/policies/privacy/" target="_blank">Gizlilik Politikası\'na</a> ' + 3837 'tabi olacağını kabul ediyorum.', 3838 'languageVal': 'Turkish (Türkçe)', 3839 'successTitle': 'Yaşasın!', 3840 'successDetails': 'En son Android geliştirici haberleri ve ipuçlarına başarıyla kaydoldunuz.' 3841 } 3842 }); 3843 break; 3844 case 'vi': 3845 window.polyglot.extend({ 3846 'newsletter': { 3847 'title': 'Nhận tin tức và mẹo mới nhất dành cho nhà phát triển Android sẽ giúp bạn tìm thấy thành công trên ' + 3848 'Google Play.', 3849 'requiredHint': '* Các trường bắt buộc', 3850 'name': 'Tên đầy đủ', 3851 'email': 'Địa chỉ email', 3852 'company': 'Tên công ty/nhà phát triển', 3853 'appUrl': 'Một trong số các URL ứng dụng trên cửa hàng Play của bạn', 3854 'business': { 3855 'label': 'Lựa chọn nào sau đây mô tả chính xác nhất doanh nghiệp của bạn?', 3856 'apps': 'Ứng dụng', 3857 'games': 'Trò chơi', 3858 'both': 'Ứng dụng và trò chơi' 3859 }, 3860 'confirmMailingList': 'Thêm tôi vào danh sách gửi thư cho bản tin hàng tháng và email định kỳ về việc phát ' + 3861 'triển và cơ hội của Google Play.', 3862 'privacyPolicy': 'Tôi xác nhận rằng thông tin được cung cấp trong biểu mẫu này tuân thủ chính sách bảo mật ' + 3863 'của <a href="https://www.google.com/intl/vi/policies/privacy/" target="_blank">Google</a>.', 3864 'languageVal': 'Vietnamese (tiếng Việt)', 3865 'successTitle': 'Thật tuyệt!', 3866 'successDetails': 'Bạn đã đăng ký thành công nhận tin tức và mẹo mới nhất dành cho nhà phát triển của Android.' 3867 } 3868 }); 3869 break; 3870} 3871 3872(function($) { 3873 'use strict'; 3874 3875 function Modal(el, options) { 3876 this.el = $(el); 3877 this.options = $.extend({}, options); 3878 this.isOpen = false; 3879 3880 this.el.on('click', function(event) { 3881 if (!$.contains(this.el.find('.dac-modal-window')[0], event.target)) { 3882 return this.el.trigger('modal-close'); 3883 } 3884 }.bind(this)); 3885 3886 this.el.on('modal-open', this.open_.bind(this)); 3887 this.el.on('modal-close', this.close_.bind(this)); 3888 this.el.on('modal-toggle', this.toggle_.bind(this)); 3889 } 3890 3891 Modal.prototype.toggle_ = function() { 3892 this.el.trigger('modal-' + (this.isOpen ? 'close' : 'open')); 3893 }; 3894 3895 Modal.prototype.close_ = function() { 3896 // When closing the modal for Android Studio downloads, reload the page 3897 // because otherwise we might get stuck with post-download dialog state 3898 if ($("[data-modal='studio_tos'].dac-active").length) { 3899 location.reload(); 3900 } 3901 this.el.removeClass('dac-active'); 3902 $('body').removeClass('dac-modal-open'); 3903 this.isOpen = false; 3904 }; 3905 3906 Modal.prototype.open_ = function() { 3907 this.el.addClass('dac-active'); 3908 $('body').addClass('dac-modal-open'); 3909 this.isOpen = true; 3910 }; 3911 3912 function onClickToggleModal(event) { 3913 event.preventDefault(); 3914 var toggle = $(event.currentTarget); 3915 var options = toggle.data(); 3916 var modal = options.modalToggle ? $('[data-modal="' + options.modalToggle + '"]') : 3917 toggle.closest('[data-modal]'); 3918 modal.trigger('modal-toggle'); 3919 } 3920 3921 /** 3922 * jQuery plugin 3923 * @param {object} options - Override default options. 3924 */ 3925 $.fn.dacModal = function(options) { 3926 return this.each(function() { 3927 new Modal(this, options); 3928 }); 3929 }; 3930 3931 $.fn.dacToggleModal = function(options) { 3932 return this.each(function() { 3933 new ToggleModal(this, options); 3934 }); 3935 }; 3936 3937 /** 3938 * Data Attribute API 3939 */ 3940 $(document).on('ready.aranja', function() { 3941 $('[data-modal]').each(function() { 3942 $(this).dacModal($(this).data()); 3943 }); 3944 3945 $('html').on('click.modal', '[data-modal-toggle]', onClickToggleModal); 3946 3947 // Check if url anchor is targetting a toggle to open the modal. 3948 if (location.hash) { 3949 var $elem = $(document.getElementById(location.hash.substr(1))); 3950 if ($elem.attr('data-modal-toggle')) { 3951 $elem.trigger('click'); 3952 } 3953 } 3954 3955 var isTargetLangValid = false; 3956 $(ANDROID_LANGUAGES).each(function(index, langCode) { 3957 if (langCode == window.getLangTarget()) { 3958 isTargetLangValid = true; 3959 return; 3960 } 3961 }); 3962 if (window.getLangTarget() !== window.getLangPref() && isTargetLangValid) { 3963 $('#langform').trigger('modal-open'); 3964 $("#langform button.yes").attr("onclick","window.changeLangPref('" + window.getLangTarget() + "', true); return false;"); 3965 $("#langform button.no").attr("onclick","window.changeLangPref('" + window.getLangPref() + "', true); return false;"); 3966 } 3967 3968 /* Check the current API level, but only if we're in the reference */ 3969 if (location.pathname.indexOf('/reference') == 0) { 3970 // init available apis based on user pref 3971 changeApiLevel(); 3972 } 3973 }); 3974})(jQuery); 3975 3976/* Fullscreen - Toggle fullscreen mode for reference pages */ 3977(function($) { 3978 'use strict'; 3979 3980 /** 3981 * @param {HTMLElement} el - The DOM element. 3982 * @constructor 3983 */ 3984 function Fullscreen(el) { 3985 this.el = $(el); 3986 this.html = $('html'); 3987 this.icon = this.el.find('.dac-sprite'); 3988 this.isFullscreen = window.readCookie(Fullscreen.COOKIE_) === 'true'; 3989 this.activate_(); 3990 this.el.on('click.dac-fullscreen', this.toggleHandler_.bind(this)); 3991 } 3992 3993 /** 3994 * Cookie name for storing the state 3995 * @type {string} 3996 * @private 3997 */ 3998 Fullscreen.COOKIE_ = 'fullscreen'; 3999 4000 /** 4001 * Classes to modify the DOM 4002 * @type {{mode: string, fullscreen: string, fullscreenExit: string}} 4003 * @private 4004 */ 4005 Fullscreen.CLASSES_ = { 4006 mode: 'dac-fullscreen-mode', 4007 fullscreen: 'dac-fullscreen', 4008 fullscreenExit: 'dac-fullscreen-exit' 4009 }; 4010 4011 /** 4012 * Event listener for toggling fullscreen mode 4013 * @param {MouseEvent} event 4014 * @private 4015 */ 4016 Fullscreen.prototype.toggleHandler_ = function(event) { 4017 event.stopPropagation(); 4018 this.toggle(!this.isFullscreen, true); 4019 }; 4020 4021 /** 4022 * Change the DOM based on current state. 4023 * @private 4024 */ 4025 Fullscreen.prototype.activate_ = function() { 4026 this.icon.toggleClass(Fullscreen.CLASSES_.fullscreen, !this.isFullscreen); 4027 this.icon.toggleClass(Fullscreen.CLASSES_.fullscreenExit, this.isFullscreen); 4028 this.html.toggleClass(Fullscreen.CLASSES_.mode, this.isFullscreen); 4029 }; 4030 4031 /** 4032 * Toggle fullscreen mode and store the state in a cookie. 4033 */ 4034 Fullscreen.prototype.toggle = function() { 4035 this.isFullscreen = !this.isFullscreen; 4036 window.writeCookie(Fullscreen.COOKIE_, this.isFullscreen, null); 4037 this.activate_(); 4038 }; 4039 4040 /** 4041 * jQuery plugin 4042 */ 4043 $.fn.dacFullscreen = function() { 4044 return this.each(function() { 4045 new Fullscreen($(this)); 4046 }); 4047 }; 4048})(jQuery); 4049 4050(function($) { 4051 'use strict'; 4052 4053 /** 4054 * @param {HTMLElement} selected - The link that is selected in the nav. 4055 * @constructor 4056 */ 4057 function HeaderTabs(selected) { 4058 4059 // Don't highlight any tabs on the index page 4060 if (location.pathname === '/index.html' || location.pathname === '/') { 4061 //return; 4062 } 4063 4064 this.selected = $(selected); 4065 this.selectedParent = this.selected.closest('.dac-nav-secondary').siblings('a'); 4066 this.links = $('.dac-header-tabs a'); 4067 4068 this.selectActiveTab(); 4069 } 4070 4071 HeaderTabs.prototype.selectActiveTab = function() { 4072 var section = null; 4073 4074 if (this.selectedParent.length) { 4075 section = this.selectedParent.text(); 4076 } else { 4077 section = this.selected.text(); 4078 } 4079 4080 if (section) { 4081 this.links.removeClass('selected'); 4082 4083 this.links.filter(function() { 4084 return $(this).text() === $.trim(section); 4085 }).addClass('selected'); 4086 } 4087 }; 4088 4089 /** 4090 * jQuery plugin 4091 */ 4092 $.fn.dacHeaderTabs = function() { 4093 return this.each(function() { 4094 new HeaderTabs(this); 4095 }); 4096 }; 4097})(jQuery); 4098 4099(function($) { 4100 'use strict'; 4101 var icon = $('<i/>').addClass('dac-sprite dac-nav-forward'); 4102 var config = JSON.parse(window.localStorage.getItem('global-navigation') || '{}'); 4103 var forwardLink = $('<span/>') 4104 .addClass('dac-nav-link-forward') 4105 .html(icon) 4106 .attr('tabindex', 0) 4107 .on('click keypress', function(e) { 4108 if (e.type == 'keypress' && e.which == 13 || e.type == 'click') { 4109 swap_(e); 4110 } 4111 }); 4112 4113 /** 4114 * @constructor 4115 */ 4116 function Nav(navigation) { 4117 $('.dac-nav-list').dacCurrentPage().dacHeaderTabs().dacSidebarToggle($('body')); 4118 4119 navigation.find('[data-reference-tree]').dacReferenceNav(); 4120 4121 setupViews_(navigation.children().eq(0).children()); 4122 4123 initCollapsedNavs(navigation.find('.dac-nav-sub-slider')); 4124 4125 $('#dac-main-navigation').scrollIntoView('.selected') 4126 } 4127 4128 function updateStore(icon) { 4129 var navClass = getCurrentLandingPage_(icon); 4130 var isExpanded = icon.hasClass('dac-expand-less-black'); 4131 var expandedNavs = config.expanded || []; 4132 if (isExpanded) { 4133 expandedNavs.push(navClass); 4134 } else { 4135 expandedNavs = expandedNavs.filter(function(item) { 4136 return item !== navClass; 4137 }); 4138 } 4139 config.expanded = expandedNavs; 4140 window.localStorage.setItem('global-navigation', JSON.stringify(config)); 4141 } 4142 4143 function toggleSubNav_(icon) { 4144 var isExpanded = icon.hasClass('dac-expand-less-black'); 4145 icon.toggleClass('dac-expand-less-black', !isExpanded); 4146 icon.toggleClass('dac-expand-more-black', isExpanded); 4147 icon.data('sub-navigation.dac').slideToggle(200); 4148 4149 updateStore(icon); 4150 } 4151 4152 function handleSubNavToggle_(event) { 4153 event.preventDefault(); 4154 var icon = $(event.target); 4155 toggleSubNav_(icon); 4156 } 4157 4158 function getCurrentLandingPage_(icon) { 4159 return icon.closest('li')[0].className.replace('dac-nav-item ', ''); 4160 } 4161 4162 // Setup sub navigation collapse/expand 4163 function initCollapsedNavs(toggleIcons) { 4164 toggleIcons.each(setInitiallyActive_($('body'))); 4165 toggleIcons.on('click keypress', function(e) { 4166 if (e.type == 'keypress' && e.which == 13 || e.type == 'click') { 4167 handleSubNavToggle_(e); 4168 } 4169 }); 4170 } 4171 4172 function setInitiallyActive_(body) { 4173 var expandedNavs = config.expanded || []; 4174 return function(i, icon) { 4175 icon = $(icon); 4176 var subNav = icon.next(); 4177 4178 if (!subNav.length) { 4179 return; 4180 } 4181 4182 var landingPageClass = getCurrentLandingPage_(icon); 4183 var expanded = expandedNavs.indexOf(landingPageClass) >= 0; 4184 landingPageClass = landingPageClass === 'home' ? 'about' : landingPageClass; 4185 4186 if (landingPageClass == 'about' && location.pathname == '/index.html') { 4187 expanded = true; 4188 } 4189 4190 // TODO: Should read from localStorage 4191 var visible = body.hasClass(landingPageClass) || expanded; 4192 4193 icon.data('sub-navigation.dac', subNav); 4194 icon.toggleClass('dac-expand-less-black', visible); 4195 icon.toggleClass('dac-expand-more-black', !visible); 4196 subNav.toggle(visible); 4197 }; 4198 } 4199 4200 function setupViews_(views) { 4201 if (views.length === 1) { 4202 // Active tier 1 nav. 4203 views.addClass('dac-active'); 4204 } else { 4205 // Activate back button and tier 2 nav. 4206 views.slice(0, 2).addClass('dac-active'); 4207 var selectedNav = views.eq(2).find('.selected').after(forwardLink); 4208 var langAttr = selectedNav.attr(window.getLangPref() + '-lang'); 4209 //form the label from locale attr if possible, else set to selectedNav text value 4210 if ((typeof langAttr !== typeof undefined && langAttr !== false) && (langAttr !== '')) { 4211 $('.dac-nav-back-title').text(langAttr); 4212 } else { 4213 $('.dac-nav-back-title').text(selectedNav.text()); 4214 } 4215 } 4216 4217 // Navigation should animate. 4218 setTimeout(function() { 4219 views.removeClass('dac-no-anim'); 4220 }, 10); 4221 } 4222 4223 function swap_(event) { 4224 event.preventDefault(); 4225 $(event.currentTarget).trigger('swap-content'); 4226 } 4227 4228 /** 4229 * jQuery plugin 4230 */ 4231 $.fn.dacNav = function() { 4232 return this.each(function() { 4233 new Nav($(this)); 4234 }); 4235 }; 4236})(jQuery); 4237 4238/* global NAVTREE_DATA */ 4239(function($) { 4240 /** 4241 * Build the reference navigation with namespace dropdowns. 4242 * @param {jQuery} el - The DOM element. 4243 */ 4244 function buildReferenceNav(el) { 4245 var supportLibraryPath = '/reference/android/support/'; 4246 var currPath = location.pathname; 4247 4248 if (currPath.indexOf(supportLibraryPath) > -1) { 4249 updateSupportLibrariesNav(supportLibraryPath, currPath); 4250 } 4251 var namespaceList = el.find('[data-reference-namespaces]'); 4252 var resources = $('[data-reference-resources]').detach(); 4253 var selected = namespaceList.find('.selected'); 4254 resources.appendTo(el); 4255 4256 // Links should be toggleable. 4257 namespaceList.find('a').addClass('dac-reference-nav-toggle dac-closed'); 4258 4259 // Set the path for the navtree data to use. 4260 var navtree_filepath = getNavtreeFilePath(supportLibraryPath, currPath); 4261 4262 // Load in all resources 4263 $.getScript(navtree_filepath, function(data, textStatus, xhr) { 4264 if (xhr.status === 200) { 4265 namespaceList.on( 4266 'click', 'a.dac-reference-nav-toggle', toggleResourcesHandler); 4267 } 4268 }); 4269 4270 // No setup required if no resources are present 4271 if (!resources.length) { 4272 return; 4273 } 4274 4275 // The resources should be a part of selected namespace. 4276 var overview = addResourcesToView(resources, selected); 4277 4278 // Currently viewing Overview 4279 if (location.href === overview.attr('href')) { 4280 overview.parent().addClass('selected'); 4281 } 4282 4283 // Open currently selected resource 4284 var listsToOpen = selected.children().eq(1); 4285 listsToOpen = listsToOpen.add( 4286 listsToOpen.find('.selected').parent()).show(); 4287 4288 // Mark dropdowns as open 4289 listsToOpen.prev().removeClass('dac-closed'); 4290 4291 // Scroll into view 4292 namespaceList.scrollIntoView(selected); 4293 } 4294 4295 function getNavtreeFilePath(supportLibraryPath, currPath) { 4296 var navtree_filepath = ''; 4297 var navtree_filename = 'navtree_data.js'; 4298 if (currPath.indexOf(supportLibraryPath + 'test') > -1) { 4299 navtree_filepath = supportLibraryPath + 'test/' + navtree_filename; 4300 } else if (currPath.indexOf(supportLibraryPath + 'wearable') > -1) { 4301 navtree_filepath = supportLibraryPath + 'wearable/' + navtree_filename; 4302 } else { 4303 navtree_filepath = '/' + navtree_filename; 4304 } 4305 return navtree_filepath; 4306 } 4307 4308 function updateSupportLibrariesNav(supportLibraryPath, currPath) { 4309 var navTitle = ''; 4310 if (currPath.indexOf(supportLibraryPath + 'test') > -1) { 4311 navTitle = 'Test Support APIs'; 4312 } else if (currPath.indexOf(supportLibraryPath + 'wearable') > -1) { 4313 navTitle = 'Wearable Support APIs'; 4314 } 4315 $('#api-nav-title').text(navTitle); 4316 $('#api-level-toggle').hide(); 4317 } 4318 4319 /** 4320 * Handles the toggling of resources. 4321 * @param {Event} event 4322 */ 4323 function toggleResourcesHandler(event) { 4324 event.preventDefault(); 4325 if (event.type == 'click' || event.type == 'keypress' && event.which == 13) { 4326 var el = $(this); 4327 // If resources for given namespace is not present, fetch correct data. 4328 if (this.tagName === 'A' && !this.hasResources) { 4329 addResourcesToView(buildResourcesViewForData(getDataForNamespace(el.text())), el.parent()); 4330 } 4331 4332 el.toggleClass('dac-closed').next().slideToggle(200); 4333 } 4334 } 4335 4336 /** 4337 * @param {String} namespace 4338 * @returns {Array} namespace data 4339 */ 4340 function getDataForNamespace(namespace) { 4341 var namespaceData = NAVTREE_DATA.filter(function(data) { 4342 return data[0] === namespace; 4343 }); 4344 4345 return namespaceData.length ? namespaceData[0][2] : []; 4346 } 4347 4348 /** 4349 * Build a list item for a resource 4350 * @param {Array} resource 4351 * @returns {String} 4352 */ 4353 function buildResourceItem(resource) { 4354 return '<li class="api apilevel-' + resource[3] + '"><a href="/' + resource[1] + '">' + resource[0] + '</a></li>'; 4355 } 4356 4357 /** 4358 * Build resources list items. 4359 * @param {Array} resources 4360 * @returns {String} 4361 */ 4362 function buildResourceList(resources) { 4363 return '<li><h2>' + resources[0] + '</h2><ul>' + resources[2].map(buildResourceItem).join('') + '</ul>'; 4364 } 4365 4366 /** 4367 * Build a resources view 4368 * @param {Array} data 4369 * @returns {jQuery} resources in an unordered list. 4370 */ 4371 function buildResourcesViewForData(data) { 4372 return $('<ul>' + data.map(buildResourceList).join('') + '</ul>'); 4373 } 4374 4375 /** 4376 * Add resources to a containing view. 4377 * @param {jQuery} resources 4378 * @param {jQuery} view 4379 * @returns {jQuery} the overview link. 4380 */ 4381 function addResourcesToView(resources, view) { 4382 var namespace = view.children().eq(0); 4383 var overview = $('<a href="' + namespace.attr('href') + '">Overview</a>'); 4384 4385 // Mark namespace with content; 4386 namespace[0].hasResources = true; 4387 4388 // Add correct classes / event listeners to resources. 4389 resources.prepend($('<li>').html(overview)) 4390 .find('a') 4391 .addClass('dac-reference-nav-resource') 4392 .end() 4393 .find('h2').attr('tabindex', 0) 4394 .addClass('dac-reference-nav-toggle dac-closed') 4395 .on('click keypress', toggleResourcesHandler) 4396 .end() 4397 .add(resources.find('ul')) 4398 .addClass('dac-reference-nav-resources') 4399 .end() 4400 .appendTo(view); 4401 4402 return overview; 4403 } 4404 4405 function setActiveReferencePackage(el) { 4406 var packageLinkEls = el.find('[data-reference-namespaces] a'); 4407 var selected = null; 4408 var highestMatchCount = 0; 4409 packageLinkEls.each(function(index, linkEl) { 4410 var matchCount = 0; 4411 $(location.pathname.split('/')).each(function(index, subpath) { 4412 if (linkEl.href.indexOf('/' + subpath + '/') > -1) { 4413 matchCount++; 4414 } 4415 }); 4416 if (matchCount > highestMatchCount) { 4417 selected = linkEl; 4418 highestMatchCount = matchCount; 4419 } 4420 }); 4421 $(selected).parent().addClass('selected'); 4422 } 4423 4424 /** 4425 * jQuery plugin 4426 */ 4427 $.fn.dacReferenceNav = function() { 4428 return this.each(function() { 4429 setActiveReferencePackage($(this)); 4430 buildReferenceNav($(this)); 4431 }); 4432 }; 4433})(jQuery); 4434 4435/** Scroll a container to make a target element visible 4436 This is called when the page finished loading. */ 4437$.fn.scrollIntoView = function(target) { 4438 if ('string' === typeof target) { 4439 target = this.find(target); 4440 } 4441 if (this.is(':visible')) { 4442 if (target.length == 0) { 4443 // If no selected item found, exit 4444 return; 4445 } 4446 4447 // get the target element's offset from its container nav by measuring the element's offset 4448 // relative to the document then subtract the container nav's offset relative to the document 4449 var targetOffset = target.offset().top - this.offset().top; 4450 var containerHeight = this.height(); 4451 if (targetOffset > containerHeight * .8) { // multiply nav height by .8 so we move up the item 4452 // if it's more than 80% down the nav 4453 // scroll the item up by an amount equal to 80% the container height 4454 this.scrollTop(targetOffset - (containerHeight * .8)); 4455 } 4456 } 4457}; 4458 4459(function($) { 4460 $.fn.dacCurrentPage = function() { 4461 // Highlight the header tabs... 4462 // highlight Design tab 4463 var baseurl = getBaseUri(window.location.pathname); 4464 var urlSegments = baseurl.split('/'); 4465 var navEl = this; 4466 var body = $('body'); 4467 var subNavEl = navEl.find('.dac-nav-secondary'); 4468 var parentNavEl; 4469 var selected; 4470 // In NDK docs, highlight appropriate sub-nav 4471 if (body.hasClass('dac-ndk')) { 4472 if (body.hasClass('guide')) { 4473 selected = navEl.find('> li.guides > a').addClass('selected'); 4474 } else if (body.hasClass('reference')) { 4475 selected = navEl.find('> li.reference > a').addClass('selected'); 4476 } else if (body.hasClass('samples')) { 4477 selected = navEl.find('> li.samples > a').addClass('selected'); 4478 } else if (body.hasClass('downloads')) { 4479 selected = navEl.find('> li.downloads > a').addClass('selected'); 4480 } 4481 } else if (body.hasClass('dac-studio')) { 4482 if (body.hasClass('download')) { 4483 selected = navEl.find('> li.download > a').addClass('selected'); 4484 } else if (body.hasClass('features')) { 4485 selected = navEl.find('> li.features > a').addClass('selected'); 4486 } else if (body.hasClass('guide')) { 4487 selected = navEl.find('> li.guide > a').addClass('selected'); 4488 } else if (body.hasClass('preview')) { 4489 selected = navEl.find('> li.preview > a').addClass('selected'); 4490 } 4491 } else if (body.hasClass('design')) { 4492 selected = navEl.find('> li.design > a').addClass('selected'); 4493 // highlight Home nav 4494 } else if (body.hasClass('about') || location.pathname == '/index.html') { 4495 parentNavEl = navEl.find('> li.home > a'); 4496 parentNavEl.addClass('has-subnav'); 4497 // In Home docs, also highlight appropriate sub-nav 4498 if (urlSegments[1] === 'wear' || urlSegments[1] === 'tv' || 4499 urlSegments[1] === 'auto') { 4500 selected = subNavEl.find('li.' + urlSegments[1] + ' > a').addClass('selected'); 4501 } else if (urlSegments[1] === 'about') { 4502 selected = subNavEl.find('li.versions > a').addClass('selected'); 4503 } else { 4504 selected = parentNavEl.removeClass('has-subnav').addClass('selected'); 4505 } 4506 // highlight Develop nav 4507 } else if (body.hasClass('develop') || body.hasClass('google')) { 4508 parentNavEl = navEl.find('> li.develop > a'); 4509 parentNavEl.addClass('has-subnav'); 4510 // In Develop docs, also highlight appropriate sub-nav 4511 if (urlSegments[1] === 'training') { 4512 selected = subNavEl.find('li.training > a').addClass('selected'); 4513 } else if (urlSegments[1] === 'guide') { 4514 selected = subNavEl.find('li.guide > a').addClass('selected'); 4515 } else if (urlSegments[1] === 'reference') { 4516 // If the root is reference, but page is also part of Google Services, select Google 4517 if (body.hasClass('google')) { 4518 selected = subNavEl.find('li.google > a').addClass('selected'); 4519 } else { 4520 selected = subNavEl.find('li.reference > a').addClass('selected'); 4521 } 4522 } else if (body.hasClass('google')) { 4523 selected = subNavEl.find('li.google > a').addClass('selected'); 4524 } else if (body.hasClass('samples')) { 4525 selected = subNavEl.find('li.samples > a').addClass('selected'); 4526 } else { 4527 selected = parentNavEl.removeClass('has-subnav').addClass('selected'); 4528 } 4529 // highlight Distribute nav 4530 } else if (body.hasClass('distribute')) { 4531 parentNavEl = navEl.find('> li.distribute > a'); 4532 parentNavEl.addClass('has-subnav'); 4533 // In Distribute docs, also highlight appropriate sub-nav 4534 if (urlSegments[2] === 'users') { 4535 selected = subNavEl.find('li.users > a').addClass('selected'); 4536 } else if (urlSegments[2] === 'engage') { 4537 selected = subNavEl.find('li.engage > a').addClass('selected'); 4538 } else if (urlSegments[2] === 'monetize') { 4539 selected = subNavEl.find('li.monetize > a').addClass('selected'); 4540 } else if (urlSegments[2] === 'analyze') { 4541 selected = subNavEl.find('li.analyze > a').addClass('selected'); 4542 } else if (urlSegments[2] === 'tools') { 4543 selected = subNavEl.find('li.disttools > a').addClass('selected'); 4544 } else if (urlSegments[2] === 'stories') { 4545 selected = subNavEl.find('li.stories > a').addClass('selected'); 4546 } else if (urlSegments[2] === 'essentials') { 4547 selected = subNavEl.find('li.essentials > a').addClass('selected'); 4548 } else if (urlSegments[2] === 'googleplay') { 4549 selected = subNavEl.find('li.googleplay > a').addClass('selected'); 4550 } else { 4551 selected = parentNavEl.removeClass('has-subnav').addClass('selected'); 4552 } 4553 } else if (body.hasClass('preview')) { 4554 selected = navEl.find('> li.preview > a').addClass('selected'); 4555 } 4556 return $(selected); 4557 }; 4558})(jQuery); 4559 4560(function($) { 4561 'use strict'; 4562 4563 /** 4564 * Toggle the visabilty of the mobile navigation. 4565 * @param {HTMLElement} el - The DOM element. 4566 * @param {Object} options 4567 * @constructor 4568 */ 4569 function ToggleNav(el, options) { 4570 this.el = $(el); 4571 this.options = $.extend({}, ToggleNav.DEFAULTS_, options); 4572 this.body = $(document.body); 4573 this.navigation_ = this.body.find(this.options.navigation); 4574 this.el.on('click', this.clickHandler_.bind(this)); 4575 } 4576 4577 ToggleNav.BREAKPOINT_ = 980; 4578 4579 /** 4580 * Open on correct sizes 4581 */ 4582 function toggleSidebarVisibility(body) { 4583 var wasClosed = ('' + localStorage.getItem('navigation-open')) === 'false'; 4584 // Override the local storage setting for navigation-open for child sites 4585 // with no-subnav class. 4586 if (document.body.classList.contains('no-subnav')) { 4587 wasClosed = false; 4588 } 4589 4590 if (wasClosed) { 4591 body.removeClass(ToggleNav.DEFAULTS_.activeClass); 4592 } else if (window.innerWidth >= ToggleNav.BREAKPOINT_) { 4593 body.addClass(ToggleNav.DEFAULTS_.activeClass); 4594 } else { 4595 body.removeClass(ToggleNav.DEFAULTS_.activeClass); 4596 } 4597 } 4598 4599 /** 4600 * ToggleNav Default Settings 4601 * @type {{body: boolean, dimmer: string, navigation: string, activeClass: string}} 4602 * @private 4603 */ 4604 ToggleNav.DEFAULTS_ = { 4605 body: true, 4606 dimmer: '.dac-nav-dimmer', 4607 animatingClass: 'dac-nav-animating', 4608 navigation: '[data-dac-nav]', 4609 activeClass: 'dac-nav-open' 4610 }; 4611 4612 /** 4613 * The actual toggle logic. 4614 * @param {Event} event 4615 * @private 4616 */ 4617 ToggleNav.prototype.clickHandler_ = function(event) { 4618 event.preventDefault(); 4619 var animatingClass = this.options.animatingClass; 4620 var body = this.body; 4621 4622 body.addClass(animatingClass); 4623 body.toggleClass(this.options.activeClass); 4624 4625 setTimeout(function() { 4626 body.removeClass(animatingClass); 4627 }, this.navigation_.transitionDuration()); 4628 4629 if (window.innerWidth >= ToggleNav.BREAKPOINT_) { 4630 localStorage.setItem('navigation-open', body.hasClass(this.options.activeClass)); 4631 } 4632 }; 4633 4634 /** 4635 * jQuery plugin 4636 * @param {object} options - Override default options. 4637 */ 4638 $.fn.dacToggleMobileNav = function() { 4639 return this.each(function() { 4640 var el = $(this); 4641 new ToggleNav(el, el.data()); 4642 }); 4643 }; 4644 4645 $.fn.dacSidebarToggle = function(body) { 4646 toggleSidebarVisibility(body); 4647 $(window).on('resize', toggleSidebarVisibility.bind(null, body)); 4648 }; 4649 4650 /** 4651 * Data Attribute API 4652 */ 4653 $(function() { 4654 $('[data-dac-toggle-nav]').dacToggleMobileNav(); 4655 }); 4656})(jQuery); 4657 4658(function($) { 4659 'use strict'; 4660 4661 /** 4662 * Submit the newsletter form to a Google Form. 4663 * @param {HTMLElement} el - The Form DOM element. 4664 * @constructor 4665 */ 4666 function NewsletterForm(el) { 4667 this.el = $(el); 4668 this.form = this.el.find('form'); 4669 $('<iframe/>').hide() 4670 .attr('name', 'dac-newsletter-iframe') 4671 .attr('src', '') 4672 .insertBefore(this.form); 4673 this.el.find('[data-newsletter-language]').val(window.polyglot.t('newsletter.languageVal')); 4674 this.form.on('submit', this.submitHandler_.bind(this)); 4675 } 4676 4677 /** 4678 * Milliseconds until modal has vanished after modal-close is triggered. 4679 * @type {number} 4680 * @private 4681 */ 4682 NewsletterForm.CLOSE_DELAY_ = 300; 4683 4684 /** 4685 * Switch view to display form after close. 4686 * @private 4687 */ 4688 NewsletterForm.prototype.closeHandler_ = function() { 4689 setTimeout(function() { 4690 this.el.trigger('swap-reset'); 4691 }.bind(this), NewsletterForm.CLOSE_DELAY_); 4692 }; 4693 4694 /** 4695 * Reset the modal to initial state. 4696 * @private 4697 */ 4698 NewsletterForm.prototype.reset_ = function() { 4699 this.form.trigger('reset'); 4700 this.el.one('modal-close', this.closeHandler_.bind(this)); 4701 }; 4702 4703 /** 4704 * Display a success view on submit. 4705 * @private 4706 */ 4707 NewsletterForm.prototype.submitHandler_ = function() { 4708 this.el.one('swap-complete', this.reset_.bind(this)); 4709 this.el.trigger('swap-content'); 4710 }; 4711 4712 /** 4713 * jQuery plugin 4714 * @param {object} options - Override default options. 4715 */ 4716 $.fn.dacNewsletterForm = function(options) { 4717 return this.each(function() { 4718 new NewsletterForm(this, options); 4719 }); 4720 }; 4721 4722 /** 4723 * Data Attribute API 4724 */ 4725 $(document).on('ready.aranja', function() { 4726 $('[data-newsletter]').each(function() { 4727 $(this).dacNewsletterForm(); 4728 }); 4729 }); 4730})(jQuery); 4731 4732/* globals METADATA, YOUTUBE_RESOURCES, BLOGGER_RESOURCES */ 4733window.metadata = {}; 4734 4735/** 4736 * Prepare metadata and indices for querying. 4737 */ 4738window.metadata.prepare = (function() { 4739 // Helper functions. 4740 function mergeArrays() { 4741 return Array.prototype.concat.apply([], arguments); 4742 } 4743 4744 /** 4745 * Creates lookup maps for a resource index. 4746 * I.e. where MAP['some tag'][resource.id] === true when that resource has 'some tag'. 4747 * @param resourceDict 4748 * @returns {{}} 4749 */ 4750 function buildResourceLookupMap(resourceDict) { 4751 var map = {}; 4752 for (var key in resourceDict) { 4753 var dictForKey = {}; 4754 var srcArr = resourceDict[key]; 4755 for (var i = 0; i < srcArr.length; i++) { 4756 dictForKey[srcArr[i].index] = true; 4757 } 4758 map[key] = dictForKey; 4759 } 4760 return map; 4761 } 4762 4763 /** 4764 * Merges metadata maps for english and the current language into the global store. 4765 */ 4766 function mergeMetadataMap(name, locale) { 4767 if (locale && locale !== 'en' && METADATA[locale]) { 4768 METADATA[name] = $.extend(METADATA.en[name], METADATA[locale][name]); 4769 } else { 4770 METADATA[name] = METADATA.en[name]; 4771 } 4772 } 4773 4774 /** 4775 * Index all resources by type, url, tag and category. 4776 * @param resources 4777 */ 4778 function createIndices(resources) { 4779 // URL, type, tag and category lookups 4780 var byType = METADATA.byType = {}; 4781 var byUrl = METADATA.byUrl = {}; 4782 var byTag = METADATA.byTag = {}; 4783 var byCategory = METADATA.byCategory = {}; 4784 4785 for (var i = 0; i < resources.length; i++) { 4786 var res = resources[i]; 4787 4788 // Store index. 4789 res.index = i; 4790 4791 // Index by type. 4792 var type = res.type; 4793 if (type) { 4794 byType[type] = byType[type] || []; 4795 byType[type].push(res); 4796 } 4797 4798 // Index by tag. 4799 var tags = res.tags || []; 4800 for (var j = 0; j < tags.length; j++) { 4801 var tag = tags[j]; 4802 if (tag) { 4803 byTag[tag] = byTag[tag] || []; 4804 byTag[tag].push(res); 4805 } 4806 } 4807 4808 // Index by category. 4809 var category = res.category; 4810 if (category) { 4811 byCategory[category] = byCategory[category] || []; 4812 byCategory[category].push(res); 4813 } 4814 4815 // Index by url. 4816 var url = res.url; 4817 if (url) { 4818 res.baseUrl = url.replace(/^intl\/\w+[\/]/, ''); 4819 byUrl[res.baseUrl] = res; 4820 } 4821 } 4822 METADATA.hasType = buildResourceLookupMap(byType); 4823 METADATA.hasTag = buildResourceLookupMap(byTag); 4824 METADATA.hasCategory = buildResourceLookupMap(byCategory); 4825 } 4826 4827 return function() { 4828 // Only once. 4829 if (METADATA.all) { return; } 4830 4831 // Get current language. 4832 var locale = getLangPref(); 4833 // Merge english resources. 4834 if (useDevsiteMetadata) { 4835 var all_keys = Object.keys(METADATA['en']); 4836 METADATA.all = [] 4837 4838 $(all_keys).each(function(index, category) { 4839 if (RESERVED_METADATA_CATEGORY_NAMES.indexOf(category) == -1) { 4840 METADATA.all = mergeArrays( 4841 METADATA.all, 4842 METADATA.en[category] 4843 ); 4844 } 4845 }); 4846 4847 METADATA.all = mergeArrays( 4848 METADATA.all, 4849 YOUTUBE_RESOURCES, 4850 BLOGGER_RESOURCES, 4851 METADATA.en.extras 4852 ); 4853 } else { 4854 METADATA.all = mergeArrays( 4855 METADATA.en.about, 4856 METADATA.en.design, 4857 METADATA.en.distribute, 4858 METADATA.en.develop, 4859 YOUTUBE_RESOURCES, 4860 BLOGGER_RESOURCES, 4861 METADATA.en.extras 4862 ); 4863 } 4864 4865 // Merge local language resources. 4866 if (locale !== 'en' && METADATA[locale]) { 4867 if (useDevsiteMetadata) { 4868 all_keys = Object.keys(METADATA[locale]); 4869 $(all_keys).each(function(index, category) { 4870 if (RESERVED_METADATA_CATEGORY_NAMES.indexOf(category) == -1) { 4871 METADATA.all = mergeArrays( 4872 METADATA.all, 4873 METADATA.en[category] 4874 ); 4875 } 4876 }); 4877 4878 METADATA.all = mergeArrays( 4879 METADATA.all, 4880 METADATA[locale].extras 4881 ); 4882 } else { 4883 METADATA.all = mergeArrays( 4884 METADATA.all, 4885 METADATA[locale].about, 4886 METADATA[locale].design, 4887 METADATA[locale].distribute, 4888 METADATA[locale].develop, 4889 METADATA[locale].extras 4890 ); 4891 4892 } 4893 } 4894 4895 mergeMetadataMap('collections', locale); 4896 mergeMetadataMap('searchHeroCollections', locale); 4897 mergeMetadataMap('carousel', locale); 4898 4899 // Create query indicies for resources. 4900 createIndices(METADATA.all, locale); 4901 4902 // Reference metadata. 4903 METADATA.androidReference = mergeArrays( 4904 window.DATA, window.SUPPORT_WEARABLE_DATA, window.SUPPORT_TEST_DATA); 4905 METADATA.googleReference = mergeArrays(window.GMS_DATA, window.GCM_DATA); 4906 }; 4907})(); 4908 4909/* global METADATA, util */ 4910window.metadata.query = (function($) { 4911 var pageMap = {}; 4912 4913 function buildResourceList(opts) { 4914 window.metadata.prepare(); 4915 var expressions = parseResourceQuery(opts.query || ''); 4916 var instanceMap = {}; 4917 var results = []; 4918 4919 for (var i = 0; i < expressions.length; i++) { 4920 var clauses = expressions[i]; 4921 4922 // Get all resources for first clause 4923 var resources = getResourcesForClause(clauses.shift()); 4924 4925 // Concat to final results list 4926 results = results.concat(resources.map(filterResources(clauses, i > 0, instanceMap)).filter(filterEmpty)); 4927 } 4928 4929 // Set correct order 4930 if (opts.sortOrder && results.length) { 4931 results = opts.sortOrder === 'random' ? util.shuffle(results) : results.sort(sortResultsByKey(opts.sortOrder)); 4932 } 4933 4934 // Slice max results. 4935 if (opts.maxResults !== Infinity) { 4936 results = results.slice(0, opts.maxResults); 4937 } 4938 4939 // Remove page level duplicates 4940 if (opts.allowDuplicates === undefined || opts.allowDuplicates === 'false') { 4941 results = results.filter(removePageLevelDuplicates); 4942 4943 for (var index = 0; index < results.length; ++index) { 4944 pageMap[results[index].index] = 1; 4945 } 4946 } 4947 4948 return results; 4949 } 4950 4951 function filterResources(clauses, removeDuplicates, map) { 4952 return function(resource) { 4953 var resourceIsAllowed = true; 4954 4955 // References must be defined. 4956 if (resource === undefined) { 4957 return; 4958 } 4959 4960 // Get canonical (localized) version of resource if possible. 4961 resource = METADATA.byUrl[resource.baseUrl] || METADATA.byUrl[resource.url] || resource; 4962 4963 // Filter out resources already used 4964 if (removeDuplicates) { 4965 resourceIsAllowed = !map[resource.index]; 4966 } 4967 4968 // Must fulfill all criteria 4969 if (clauses.length > 0) { 4970 resourceIsAllowed = resourceIsAllowed && doesResourceMatchClauses(resource, clauses); 4971 } 4972 4973 // Mark resource as used. 4974 if (resourceIsAllowed) { 4975 map[resource.index] = 1; 4976 } 4977 4978 return resourceIsAllowed && resource; 4979 }; 4980 } 4981 4982 function filterEmpty(resource) { 4983 return resource; 4984 } 4985 4986 function sortResultsByKey(key) { 4987 var desc = key.charAt(0) === '-'; 4988 4989 if (desc) { 4990 key = key.substring(1); 4991 } 4992 4993 return function(x, y) { 4994 return (desc ? -1 : 1) * (parseInt(x[key], 10) - parseInt(y[key], 10)); 4995 }; 4996 } 4997 4998 function getResourcesForClause(clause) { 4999 switch (clause.attr) { 5000 case 'type': 5001 return METADATA.byType[clause.value]; 5002 case 'tag': 5003 return METADATA.byTag[clause.value]; 5004 case 'collection': 5005 var resources = METADATA.collections[clause.value] || {}; 5006 return getResourcesByUrlCollection(resources.resources); 5007 case 'history': 5008 return getResourcesByUrlCollection($.dacGetVisitedUrls(clause.value)); 5009 case 'section': 5010 return getResourcesByUrlCollection([clause.value].sections); 5011 default: 5012 return []; 5013 } 5014 } 5015 5016 function getResourcesByUrlCollection(resources) { 5017 return (resources || []).map(function(url) { 5018 return METADATA.byUrl[url]; 5019 }); 5020 } 5021 5022 function removePageLevelDuplicates(resource) { 5023 return resource && !pageMap[resource.index]; 5024 } 5025 5026 function doesResourceMatchClauses(resource, clauses) { 5027 for (var i = 0; i < clauses.length; i++) { 5028 var map; 5029 switch (clauses[i].attr) { 5030 case 'type': 5031 map = METADATA.hasType[clauses[i].value]; 5032 break; 5033 case 'tag': 5034 map = METADATA.hasTag[clauses[i].value]; 5035 break; 5036 } 5037 5038 if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) { 5039 return clauses[i].negative; 5040 } 5041 } 5042 5043 return true; 5044 } 5045 5046 function parseResourceQuery(query) { 5047 // Parse query into array of expressions (expression e.g. 'tag:foo + type:video') 5048 var expressions = []; 5049 var expressionStrs = query.split(',') || []; 5050 for (var i = 0; i < expressionStrs.length; i++) { 5051 var expr = expressionStrs[i] || ''; 5052 5053 // Break expression into clauses (clause e.g. 'tag:foo') 5054 var clauses = []; 5055 var clauseStrs = expr.split(/(?=[\+\-])/); 5056 for (var j = 0; j < clauseStrs.length; j++) { 5057 var clauseStr = clauseStrs[j] || ''; 5058 5059 // Get attribute and value from clause (e.g. attribute='tag', value='foo') 5060 var parts = clauseStr.split(':'); 5061 var clause = {}; 5062 5063 clause.attr = parts[0].replace(/^\s+|\s+$/g, ''); 5064 if (clause.attr) { 5065 if (clause.attr.charAt(0) === '+') { 5066 clause.attr = clause.attr.substring(1); 5067 } else if (clause.attr.charAt(0) === '-') { 5068 clause.negative = true; 5069 clause.attr = clause.attr.substring(1); 5070 } 5071 } 5072 5073 if (parts.length > 1) { 5074 clause.value = parts[1].replace(/^\s+|\s+$/g, ''); 5075 } 5076 5077 clauses.push(clause); 5078 } 5079 5080 if (!clauses.length) { 5081 continue; 5082 } 5083 5084 expressions.push(clauses); 5085 } 5086 5087 return expressions; 5088 } 5089 5090 return buildResourceList; 5091})(jQuery); 5092 5093/* global METADATA, getLangPref */ 5094 5095window.metadata.search = (function() { 5096 'use strict'; 5097 5098 var currentLang = getLangPref(); 5099 5100 function search(query) { 5101 window.metadata.prepare(); 5102 return { 5103 android: findDocsMatches(query, METADATA.androidReference), 5104 docs: findDocsMatches(query, METADATA.googleReference), 5105 resources: findResourceMatches(query) 5106 }; 5107 } 5108 5109 function findDocsMatches(query, data) { 5110 var results = []; 5111 5112 for (var i = 0; i < data.length; i++) { 5113 var s = data[i]; 5114 if (query.length !== 0 && s.label.toLowerCase().indexOf(query.toLowerCase()) !== -1) { 5115 results.push(s); 5116 } 5117 } 5118 5119 rankAutocompleteApiResults(query, results); 5120 5121 return results; 5122 } 5123 5124 function findResourceMatches(query) { 5125 var results = []; 5126 5127 // Search for matching JD docs 5128 if (query.length >= 2) { 5129 /* In some langs, spaces may be optional between certain non-Ascii word-glyphs. For 5130 * those langs, only match query at word boundaries if query includes Ascii chars only. 5131 */ 5132 var NO_BOUNDARY_LANGUAGES = ['ja','ko','vi','zh-cn','zh-tw']; 5133 var isAsciiOnly = /^[\u0000-\u007f]*$/.test(query); 5134 var noBoundaries = (NO_BOUNDARY_LANGUAGES.indexOf(window.getLangPref()) !== -1); 5135 var exprBoundary = (!isAsciiOnly && noBoundaries) ? '' : '(?:^|\\s)'; 5136 var queryRegex = new RegExp(exprBoundary + query.toLowerCase(), 'g'); 5137 5138 var all = METADATA.all; 5139 for (var i = 0; i < all.length; i++) { 5140 // current search comparison, with counters for tag and title, 5141 // used later to improve ranking 5142 var s = all[i]; 5143 s.matched_tag = 0; 5144 s.matched_title = 0; 5145 var matched = false; 5146 5147 // Check if query matches any tags; work backwards toward 1 to assist ranking 5148 if (s.keywords) { 5149 for (var j = s.keywords.length - 1; j >= 0; j--) { 5150 // it matches a tag 5151 if (s.keywords[j].toLowerCase().match(queryRegex)) { 5152 matched = true; 5153 s.matched_tag = j + 1; // add 1 to index position 5154 } 5155 } 5156 } 5157 5158 // Check if query matches doc title 5159 if (s.title.toLowerCase().match(queryRegex)) { 5160 matched = true; 5161 s.matched_title = 1; 5162 } 5163 5164 // Remember the doc if it matches either 5165 if (matched) { 5166 results.push(s); 5167 } 5168 } 5169 5170 // Improve the current results 5171 results = lookupBetterResult(results); 5172 5173 // Rank/sort all the matched pages 5174 rankAutocompleteDocResults(results); 5175 5176 return results; 5177 } 5178 } 5179 5180 // Replaces a match with another resource by url, if it exists. 5181 function lookupReplacementByUrl(match, url) { 5182 var replacement = METADATA.byUrl[url]; 5183 5184 // Replacement resource does not exists. 5185 if (!replacement) { return; } 5186 5187 replacement.matched_title = Math.max(replacement.matched_title, match.matched_title); 5188 replacement.matched_tag = Math.max(replacement.matched_tag, match.matched_tag); 5189 5190 return replacement; 5191 } 5192 5193 // Find the localized version of a page if it exists. 5194 function lookupLocalizedVersion(match) { 5195 return METADATA.byUrl[match.baseUrl] || METADATA.byUrl[match.url]; 5196 } 5197 5198 // Find the main page for a tutorial when matching a subpage. 5199 function lookupTutorialIndex(match) { 5200 // Guard for non index tutorial pages. 5201 if (match.type !== 'training' || match.url.indexOf('index.html') >= 0) { return; } 5202 5203 var indexUrl = match.url.replace(/[^\/]+$/, 'index.html'); 5204 return lookupReplacementByUrl(match, indexUrl); 5205 } 5206 5207 // Find related results which are a better match for the user. 5208 function lookupBetterResult(matches) { 5209 var newMatches = []; 5210 5211 matches = matches.filter(function(match) { 5212 var newMatch = match; 5213 newMatch = lookupTutorialIndex(newMatch) || newMatch; 5214 newMatch = lookupLocalizedVersion(newMatch) || newMatch; 5215 5216 if (newMatch !== match) { 5217 newMatches.push(newMatch); 5218 } 5219 5220 return newMatch === match; 5221 }); 5222 5223 return toUnique(newMatches.concat(matches)); 5224 } 5225 5226 /* Order the jd doc result list based on match quality */ 5227 function rankAutocompleteDocResults(matches) { 5228 if (!matches || !matches.length) { 5229 return; 5230 } 5231 5232 var _resultScoreFn = function(match) { 5233 var score = 1.0; 5234 5235 // if the query matched a tag 5236 if (match.matched_tag > 0) { 5237 // multiply score by factor relative to position in tags list (max of 3) 5238 score *= 3 / match.matched_tag; 5239 5240 // if it also matched the title 5241 if (match.matched_title > 0) { 5242 score *= 2; 5243 } 5244 } else if (match.matched_title > 0) { 5245 score *= 3; 5246 } 5247 5248 if (match.lang === currentLang) { 5249 score *= 5; 5250 } 5251 5252 return score; 5253 }; 5254 5255 for (var i = 0; i < matches.length; i++) { 5256 matches[i].__resultScore = _resultScoreFn(matches[i]); 5257 } 5258 5259 matches.sort(function(a, b) { 5260 var n = b.__resultScore - a.__resultScore; 5261 5262 if (n === 0) { 5263 // lexicographical sort if scores are the same 5264 n = (a.title < b.title) ? -1 : 1; 5265 } 5266 5267 return n; 5268 }); 5269 } 5270 5271 /* Order the result list based on match quality */ 5272 function rankAutocompleteApiResults(query, matches) { 5273 query = query || ''; 5274 if (!matches || !matches.length) { 5275 return; 5276 } 5277 5278 // helper function that gets the last occurence index of the given regex 5279 // in the given string, or -1 if not found 5280 var _lastSearch = function(s, re) { 5281 if (s === '') { 5282 return -1; 5283 } 5284 var l = -1; 5285 var tmp; 5286 while ((tmp = s.search(re)) >= 0) { 5287 if (l < 0) { 5288 l = 0; 5289 } 5290 l += tmp; 5291 s = s.substr(tmp + 1); 5292 } 5293 return l; 5294 }; 5295 5296 // helper function that counts the occurrences of a given character in 5297 // a given string 5298 var _countChar = function(s, c) { 5299 var n = 0; 5300 for (var i = 0; i < s.length; i++) { 5301 if (s.charAt(i) === c) { 5302 ++n; 5303 } 5304 } 5305 return n; 5306 }; 5307 5308 var queryLower = query.toLowerCase(); 5309 var queryAlnum = (queryLower.match(/\w+/) || [''])[0]; 5310 var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum); 5311 var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b'); 5312 5313 var _resultScoreFn = function(result) { 5314 // scores are calculated based on exact and prefix matches, 5315 // and then number of path separators (dots) from the last 5316 // match (i.e. favoring classes and deep package names) 5317 var score = 1.0; 5318 var labelLower = result.label.toLowerCase(); 5319 var t; 5320 var partsAfter; 5321 t = _lastSearch(labelLower, partExactAlnumRE); 5322 if (t >= 0) { 5323 // exact part match 5324 partsAfter = _countChar(labelLower.substr(t + 1), '.'); 5325 score *= 200 / (partsAfter + 1); 5326 } else { 5327 t = _lastSearch(labelLower, partPrefixAlnumRE); 5328 if (t >= 0) { 5329 // part prefix match 5330 partsAfter = _countChar(labelLower.substr(t + 1), '.'); 5331 score *= 20 / (partsAfter + 1); 5332 } 5333 } 5334 5335 return score; 5336 }; 5337 5338 for (var i = 0; i < matches.length; i++) { 5339 // if the API is deprecated, default score is 0; otherwise, perform scoring 5340 if (matches[i].deprecated === 'true') { 5341 matches[i].__resultScore = 0; 5342 } else { 5343 matches[i].__resultScore = _resultScoreFn(matches[i]); 5344 } 5345 } 5346 5347 matches.sort(function(a, b) { 5348 var n = b.__resultScore - a.__resultScore; 5349 5350 if (n === 0) { 5351 // lexicographical sort if scores are the same 5352 n = (a.label < b.label) ? -1 : 1; 5353 } 5354 5355 return n; 5356 }); 5357 } 5358 5359 // Destructive but fast toUnique. 5360 // http://stackoverflow.com/a/25082874 5361 function toUnique(array) { 5362 var c; 5363 var b = array.length || 1; 5364 5365 while (c = --b) { 5366 while (c--) { 5367 if (array[b] === array[c]) { 5368 array.splice(c, 1); 5369 } 5370 } 5371 } 5372 return array; 5373 } 5374 5375 return search; 5376})(); 5377 5378(function($) { 5379 'use strict'; 5380 5381 /** 5382 * Smoothly scroll to location on current page. 5383 * @param el 5384 * @param options 5385 * @constructor 5386 */ 5387 function ScrollButton(el, options) { 5388 this.el = $(el); 5389 this.target = $(this.el.attr('href')); 5390 this.options = $.extend({}, ScrollButton.DEFAULTS_, options); 5391 5392 if (typeof this.options.offset === 'string') { 5393 this.options.offset = $(this.options.offset).height(); 5394 } 5395 5396 this.el.on('click', this.clickHandler_.bind(this)); 5397 } 5398 5399 /** 5400 * Default options 5401 * @type {{duration: number, easing: string, offset: number, scrollContainer: string}} 5402 * @private 5403 */ 5404 ScrollButton.DEFAULTS_ = { 5405 duration: 300, 5406 easing: 'swing', 5407 offset: '.dac-header', 5408 scrollContainer: 'html, body' 5409 }; 5410 5411 /** 5412 * Scroll logic 5413 * @param event 5414 * @private 5415 */ 5416 ScrollButton.prototype.clickHandler_ = function(event) { 5417 if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { 5418 return; 5419 } 5420 5421 event.preventDefault(); 5422 5423 var position = this.getTargetPosition(); 5424 $(this.options.scrollContainer).animate({ 5425 scrollTop: position - this.options.offset 5426 }, this.options); 5427 }; 5428 5429 ScrollButton.prototype.getTargetPosition = function() { 5430 if (this.options.scrollContainer === ScrollButton.DEFAULTS_.scrollContainer) { 5431 return this.target.offset().top; 5432 } 5433 var scrollContainer = $(this.options.scrollContainer)[0]; 5434 var currentEl = this.target[0]; 5435 var pos = 0; 5436 while (currentEl !== scrollContainer && currentEl !== null) { 5437 pos += currentEl.offsetTop; 5438 currentEl = currentEl.offsetParent; 5439 } 5440 return pos; 5441 }; 5442 5443 /** 5444 * jQuery plugin 5445 * @param {object} options - Override default options. 5446 */ 5447 $.fn.dacScrollButton = function(options) { 5448 return this.each(function() { 5449 new ScrollButton(this, options); 5450 }); 5451 }; 5452 5453 /** 5454 * Data Attribute API 5455 */ 5456 $(document).on('ready.aranja', function() { 5457 $('[data-scroll-button]').each(function() { 5458 $(this).dacScrollButton($(this).data()); 5459 }); 5460 }); 5461})(jQuery); 5462 5463/* global getLangPref */ 5464(function($) { 5465 var LANG; 5466 5467 function getSearchLang() { 5468 if (!LANG) { 5469 LANG = getLangPref(); 5470 5471 // Fix zh-cn to be zh-CN. 5472 LANG = LANG.replace(/-\w+/, function(m) { return m.toUpperCase(); }); 5473 } 5474 return LANG; 5475 } 5476 5477 function customSearch(query, start) { 5478 var searchParams = { 5479 // current cse instance: 5480 //cx: '001482626316274216503:zu90b7s047u', 5481 // new cse instance: 5482 cx: '000521750095050289010:zpcpi1ea4s8', 5483 key: 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8', 5484 q: query, 5485 start: start || 1, 5486 num: 9, 5487 hl: getSearchLang(), 5488 fields: 'queries,items(pagemap,link,title,htmlSnippet,formattedUrl)' 5489 }; 5490 5491 return $.get('https://content.googleapis.com/customsearch/v1?' + $.param(searchParams)); 5492 } 5493 5494 function renderResults(el, results, searchAppliance) { 5495 var referenceResults = searchAppliance.getReferenceResults(); 5496 if (!results.items) { 5497 el.append($('<div>').text('No results')); 5498 return; 5499 } 5500 5501 for (var i = 0; i < results.items.length; i++) { 5502 var item = results.items[i]; 5503 var isDuplicate = false; 5504 $(referenceResults.android).each(function(index, result) { 5505 if (item.link.indexOf(result.link) > -1) { 5506 isDuplicate = true; 5507 return false; 5508 } 5509 }); 5510 5511 if (!isDuplicate) { 5512 var hasImage = item.pagemap && item.pagemap.cse_thumbnail; 5513 var sectionMatch = item.link.match(/developer\.android\.com\/(\w*)/); 5514 var section = (sectionMatch && sectionMatch[1]) || 'blog'; 5515 5516 var entry = $('<div>').addClass('dac-custom-search-entry cols'); 5517 5518 if (hasImage) { 5519 var image = item.pagemap.cse_thumbnail[0]; 5520 entry.append($('<div>').addClass('dac-custom-search-image-wrapper') 5521 .append($('<div>').addClass('dac-custom-search-image').css('background-image', 'url(' + image.src + ')'))); 5522 } 5523 5524 entry.append($('<div>').addClass('dac-custom-search-text-wrapper') 5525 .append($('<p>').addClass('dac-custom-search-section').text(section)) 5526 .append( 5527 $('<a>').text(item.title).attr('href', item.link).wrap('<h2>').parent().addClass('dac-custom-search-title') 5528 ) 5529 .append($('<p>').addClass('dac-custom-search-snippet').html(item.htmlSnippet.replace(/<br>/g, ''))) 5530 .append($('<a>').addClass('dac-custom-search-link').text(item.formattedUrl).attr('href', item.link))); 5531 5532 el.append(entry); 5533 } 5534 } 5535 5536 if (results.queries.nextPage) { 5537 var loadMoreButton = $('<button id="dac-custom-search-load-more">') 5538 .addClass('dac-custom-search-load-more') 5539 .text('Load more') 5540 .click(function() { 5541 loadMoreResults(el, results, searchAppliance); 5542 }); 5543 5544 el.append(loadMoreButton); 5545 } 5546 }; 5547 5548 function loadMoreResults(el, results, searchAppliance) { 5549 var query = results.queries.request[0].searchTerms; 5550 var start = results.queries.nextPage[0].startIndex; 5551 var loadMoreButton = el.find('#dac-custom-search-load-more'); 5552 5553 loadMoreButton.text('Loading more...'); 5554 5555 customSearch(query, start).then(function(results) { 5556 loadMoreButton.remove(); 5557 renderResults(el, results, searchAppliance); 5558 }); 5559 } 5560 5561 $.fn.customSearch = function(query, searchAppliance) { 5562 var el = $(this); 5563 5564 customSearch(query).then(function(results) { 5565 el.empty(); 5566 renderResults(el, results, searchAppliance); 5567 }); 5568 }; 5569})(jQuery); 5570 5571/* global METADATA */ 5572 5573(function($) { 5574 $.fn.dacSearchRenderHero = function(resources, query) { 5575 var el = $(this); 5576 el.empty(); 5577 5578 var resource = METADATA.searchHeroCollections[query]; 5579 5580 if (resource) { 5581 el.dacHero(resource, true); 5582 el.show(); 5583 5584 return true; 5585 } else { 5586 el.hide(); 5587 } 5588 }; 5589})(jQuery); 5590 5591(function($) { 5592 $.fn.dacSearchRenderReferences = function(results, query) { 5593 var referenceCard = $('.suggest-card.reference'); 5594 referenceCard.data('searchreferences.dac', {results: results, query: query}); 5595 renderResults(referenceCard, results, query, false); 5596 }; 5597 5598 var ROW_COUNT_COLLAPSED = 20; 5599 var ROW_COUNT_EXPANDED = 40; 5600 var ROW_COUNT_GOOGLE_COLLAPSED = 1; 5601 var ROW_COUNT_GOOGLE_EXPANDED = 8; 5602 5603 function onSuggestionClick(e) { 5604 devsite.analytics.trackAnalyticsEvent('event', 5605 'Suggestion Click', 'clicked: ' + $(e.currentTarget).attr('href'), 5606 'query: ' + $('#search_autocomplete').val().toLowerCase()); 5607 } 5608 5609 function buildLink(match) { 5610 var link = $('<a>').attr('href', window.toRoot + match.link); 5611 5612 var label = match.label; 5613 var classNameStart = label.match(/[A-Z]/) ? label.search(/[A-Z]/) : label.lastIndexOf('.') + 1; 5614 var newLink = '<span class="namespace">' + 5615 label.substr(0, classNameStart) + 5616 '</span>' + 5617 label.substr(classNameStart, label.length); 5618 5619 link.html(newLink); 5620 return link; 5621 } 5622 5623 function buildSuggestion(match, query) { 5624 var li = $('<li>').addClass('dac-search-results-reference-entry'); 5625 5626 var link = buildLink(match); 5627 link.highlightMatches(query); 5628 li.append(link); 5629 return li[0]; 5630 } 5631 5632 function buildResults(results, query) { 5633 return results.map(function(match) { 5634 return buildSuggestion(match, query); 5635 }); 5636 } 5637 5638 function renderAndroidResults(list, gMatches, query) { 5639 list.empty(); 5640 5641 var header = $('<li class="dac-search-results-reference-header">android APIs</li>'); 5642 list.append(header); 5643 5644 if (gMatches.length > 0) { 5645 list.removeClass('no-results'); 5646 5647 var resources = buildResults(gMatches, query); 5648 list.append(resources); 5649 return true; 5650 } else { 5651 list.append('<li class="dac-search-results-reference-entry-empty">No results</li>'); 5652 } 5653 } 5654 5655 function renderGoogleDocsResults(list, gGoogleMatches, query) { 5656 list = $('.suggest-card.reference ul'); 5657 5658 if (gGoogleMatches.length > 0) { 5659 list.append('<li class="dac-search-results-reference-header">in Google Services</li>'); 5660 5661 var resources = buildResults(gGoogleMatches, query); 5662 list.append(resources); 5663 5664 return true; 5665 } 5666 } 5667 5668 function renderResults(referenceCard, results, query, expanded) { 5669 var list = referenceCard.find('ul'); 5670 list.toggleClass('is-expanded', !!expanded); 5671 5672 // Figure out how many results we can show in our fixed size box. 5673 var total = expanded ? ROW_COUNT_EXPANDED : ROW_COUNT_COLLAPSED; 5674 var googleCount = expanded ? ROW_COUNT_GOOGLE_EXPANDED : ROW_COUNT_GOOGLE_COLLAPSED; 5675 googleCount = Math.max(googleCount, total - results.android.length); 5676 googleCount = Math.min(googleCount, results.docs.length); 5677 5678 if (googleCount > 0) { 5679 // If there are google results, reserve space for its header. 5680 googleCount++; 5681 } 5682 5683 var androidCount = Math.max(0, total - googleCount); 5684 if (androidCount === 0) { 5685 // Reserve space for "No reference results" 5686 googleCount--; 5687 } 5688 5689 renderAndroidResults(list, results.android.slice(0, androidCount), query); 5690 renderGoogleDocsResults(list, results.docs.slice(0, googleCount - 1), query); 5691 5692 var totalResults = results.android.length + results.docs.length; 5693 if (totalResults === 0) { 5694 list.addClass('no-results'); 5695 } 5696 5697 // Tweak see more logic to account for references. 5698 var hasMore = totalResults > ROW_COUNT_COLLAPSED && !util.matchesMedia('mobile'); 5699 if (hasMore) { 5700 // We can't actually show all matches, only as many as the expanded list 5701 // will fit, so we actually lie if the total results count is more 5702 var moreCount = Math.min(totalResults, ROW_COUNT_EXPANDED + ROW_COUNT_GOOGLE_EXPANDED); 5703 var $moreLink = $('<li class="dac-search-results-reference-entry-empty " data-toggle="show-more">see more matches</li>'); 5704 list.append($moreLink.on('click', onToggleMore)); 5705 } 5706 var searchEl = $('#search-resources'); 5707 searchEl.toggleClass('dac-has-more', searchEl.hasClass('dac-has-more') || (hasMore && !expanded)); 5708 searchEl.toggleClass('dac-has-less', searchEl.hasClass('dac-has-less') || (hasMore && expanded)); 5709 } 5710 5711 function onToggleMore(e) { 5712 var link = $(e.currentTarget); 5713 var referenceCard = $('.suggest-card.reference'); 5714 var data = referenceCard.data('searchreferences.dac'); 5715 5716 if (util.matchesMedia('mobile')) { return; } 5717 5718 renderResults(referenceCard, data.results, data.query, link.data('toggle') === 'show-more'); 5719 } 5720 5721 $(document).on('click', '.dac-search-results-resources [data-toggle="show-more"]', onToggleMore); 5722 $(document).on('click', '.dac-search-results-resources [data-toggle="show-less"]', onToggleMore); 5723 $(document).on('click', '.suggest-card.reference a', onSuggestionClick); 5724})(jQuery); 5725 5726(function($) { 5727 function highlightPage(query, page) { 5728 page.find('.title').highlightMatches(query); 5729 } 5730 5731 $.fn.dacSearchRenderResources = function(gDocsMatches, query) { 5732 this.resourceWidget(gDocsMatches, { 5733 itemsPerPage: 18, 5734 initialResults: 6, 5735 cardSizes: ['6x2'], 5736 onRenderPage: highlightPage.bind(null, query) 5737 }); 5738 5739 return this; 5740 }; 5741})(jQuery); 5742 5743/*global metadata */ 5744 5745(function($, metadata) { 5746 'use strict'; 5747 5748 function Search() { 5749 this.body = $('body'); 5750 this.lastQuery = null; 5751 this.searchResults = $('#search-results'); 5752 this.searchClose = $('[data-search-close]'); 5753 this.searchClear = $('[data-search-clear]'); 5754 this.searchInput = $('#search_autocomplete'); 5755 this.searchResultsContent = $('#dac-search-results-content'); 5756 this.searchResultsFor = $('#search-results-for'); 5757 this.searchResultsHistory = $('#dac-search-results-history'); 5758 this.searchResultsResources = $('#search-resources'); 5759 this.searchResultsHero = $('#dac-search-results-hero'); 5760 this.searchResultsReference = $('#dac-search-results-reference'); 5761 this.searchHeader = $('[data-search]').data('search-input.dac'); 5762 this.pageNav = $('a[name=navigation]'); 5763 this.currQueryReferenceResults = {}; 5764 this.isOpen = false; 5765 } 5766 5767 Search.prototype.init = function() { 5768 this.searchHistory = window.dacStore('search-history'); 5769 5770 this.searchInput.focus(this.onSearchChanged.bind(this)); 5771 this.searchInput.keypress(this.handleKeyboardShortcut.bind(this)); 5772 this.pageNav.keyup(this.handleTabbedToNav.bind(this)); 5773 this.searchResults.keyup(this.handleKeyboardShortcut.bind(this)); 5774 this.searchInput.on('input', this.onSearchChanged.bind(this)); 5775 this.searchClear.click(this.clear.bind(this)); 5776 this.searchClose.click(this.close.bind(this)); 5777 5778 this.customSearch = $.fn.debounce(function(query) { 5779 $('#dac-custom-search-results').customSearch(query, this); 5780 }.bind(this), 1000); 5781 // Start search shortcut (/) 5782 $('body').keyup(function(event) { 5783 if (event.which === 191 && $(event.target).is(':not(:input)')) { 5784 this.searchInput.focus(); 5785 } 5786 }.bind(this)); 5787 5788 $(window).on('popstate', this.onPopState.bind(this)); 5789 $(window).hashchange(this.onHashChange.bind(this)); 5790 this.onHashChange(); 5791 }; 5792 5793 Search.prototype.checkRedirectToIndex = function() { 5794 var query = this.getUrlQuery(); 5795 var target = window.getLangTarget(); 5796 var prefix = (target !== 'en') ? '/intl/' + target : ''; 5797 var pathname = location.pathname.slice(prefix.length); 5798 if (query != null && pathname !== '/index.html') { 5799 location.href = prefix + '/index.html' + location.hash; 5800 return true; 5801 } 5802 }; 5803 5804 Search.prototype.handleKeyboardShortcut = function(event) { 5805 // Close (esc) 5806 if (event.which === 27) { 5807 this.searchClose.trigger('click'); 5808 event.preventDefault(); 5809 } 5810 5811 // Previous result (up arrow) 5812 if (event.which === 38) { 5813 this.previousResult(); 5814 event.preventDefault(); 5815 } 5816 5817 // Next result (down arrow) 5818 if (event.which === 40) { 5819 this.nextResult(); 5820 event.preventDefault(); 5821 } 5822 5823 // Navigate to result (enter) 5824 if (event.which === 13) { 5825 this.navigateToResult(); 5826 event.preventDefault(); 5827 } 5828 }; 5829 5830 Search.prototype.handleTabbedToNav = function(event) { 5831 if (this.isOpen) { 5832 this.searchClose.trigger('click'); 5833 } 5834 } 5835 5836 Search.prototype.goToResult = function(relativeIndex) { 5837 var links = this.searchResults.find('a').filter(':visible'); 5838 var selectedLink = this.searchResults.find('.dac-selected'); 5839 5840 if (selectedLink.length) { 5841 var found = $.inArray(selectedLink[0], links); 5842 5843 selectedLink.removeClass('dac-selected'); 5844 links.eq(found + relativeIndex).addClass('dac-selected'); 5845 return true; 5846 } else { 5847 if (relativeIndex > 0) { 5848 links.first().addClass('dac-selected'); 5849 } 5850 } 5851 }; 5852 5853 Search.prototype.previousResult = function() { 5854 this.goToResult(-1); 5855 }; 5856 5857 Search.prototype.nextResult = function() { 5858 this.goToResult(1); 5859 }; 5860 5861 Search.prototype.navigateToResult = function() { 5862 var query = this.getQuery(); 5863 var selectedLink = this.searchResults.find('.dac-selected'); 5864 5865 if (selectedLink.length) { 5866 selectedLink[0].click(); 5867 } else { 5868 this.searchHistory.push(query); 5869 this.addQueryToUrl(query); 5870 5871 var isMobileOrTablet = typeof window.orientation !== 'undefined'; 5872 5873 if (isMobileOrTablet) { 5874 this.searchInput.blur(); 5875 } 5876 } 5877 }; 5878 5879 Search.prototype.onHashChange = function() { 5880 var query = this.getUrlQuery(); 5881 if (query != null && query !== this.getQuery()) { 5882 this.searchInput.val(query); 5883 this.onSearchChanged(); 5884 } 5885 }; 5886 5887 Search.prototype.clear = function() { 5888 this.searchInput.val(''); 5889 window.location.hash = ''; 5890 this.onSearchChanged(); 5891 this.searchInput.focus(); 5892 }; 5893 5894 Search.prototype.close = function() { 5895 this.removeQueryFromUrl(); 5896 this.searchInput.blur(); 5897 this.hideOverlay(); 5898 this.pageNav.focus(); 5899 this.isOpen = false; 5900 }; 5901 5902 Search.prototype.getUrlQuery = function() { 5903 var queryMatch = location.hash.match(/q=(.*)&?/); 5904 return queryMatch && queryMatch[1] && decodeURI(queryMatch[1]); 5905 }; 5906 5907 Search.prototype.getQuery = function() { 5908 return this.searchInput.val().replace(/(^ +)|( +$)/g, ''); 5909 }; 5910 5911 Search.prototype.getReferenceResults = function() { 5912 return this.currQueryReferenceResults; 5913 }; 5914 5915 Search.prototype.onSearchChanged = function() { 5916 var query = this.getQuery(); 5917 5918 this.showOverlay(); 5919 this.render(query); 5920 }; 5921 5922 Search.prototype.render = function(query) { 5923 if (this.lastQuery === query) { return; } 5924 5925 if (query.length < 2) { 5926 query = ''; 5927 } 5928 5929 this.lastQuery = query; 5930 this.searchResultsFor.text(query); 5931 5932 // CSE results lag behind the metadata/reference results. We need to empty 5933 // the CSE results and add 'Loading' text so user's aren't looking at two 5934 // different sets of search results at one time. 5935 var $loadingEl = 5936 $('<div class="loadingCustomSearchResults">Loading Results...</div>'); 5937 $('#dac-custom-search-results').empty().prepend($loadingEl); 5938 5939 this.customSearch(query); 5940 var metadataResults = metadata.search(query); 5941 this.searchResultsResources.dacSearchRenderResources(metadataResults.resources, query); 5942 this.searchResultsReference.dacSearchRenderReferences(metadataResults, query); 5943 this.currQueryReferenceResults = metadataResults; 5944 var hasHero = this.searchResultsHero.dacSearchRenderHero(metadataResults.resources, query); 5945 var hasQuery = !!query; 5946 5947 this.searchResultsReference.toggle(!hasHero); 5948 this.searchResultsContent.toggle(hasQuery); 5949 this.searchResultsHistory.toggle(!hasQuery); 5950 this.addQueryToUrl(query); 5951 this.pushState(); 5952 }; 5953 5954 Search.prototype.addQueryToUrl = function(query) { 5955 var hash = 'q=' + encodeURI(query); 5956 5957 if (query) { 5958 if (window.history.replaceState) { 5959 window.history.replaceState(null, '', '#' + hash); 5960 } else { 5961 window.location.hash = hash; 5962 } 5963 } 5964 }; 5965 5966 Search.prototype.onPopState = function() { 5967 if (!this.getUrlQuery()) { 5968 this.hideOverlay(); 5969 this.searchHeader.unsetActiveState(); 5970 } 5971 }; 5972 5973 Search.prototype.removeQueryFromUrl = function() { 5974 window.location.hash = ''; 5975 }; 5976 5977 Search.prototype.pushState = function() { 5978 if (window.history.pushState && !this.lastQuery.length) { 5979 window.history.pushState(null, ''); 5980 } 5981 }; 5982 5983 Search.prototype.showOverlay = function() { 5984 this.isOpen = true; 5985 this.body.addClass('dac-modal-open dac-search-open'); 5986 }; 5987 5988 Search.prototype.hideOverlay = function() { 5989 this.body.removeClass('dac-modal-open dac-search-open'); 5990 }; 5991 5992 $(document).on('ready.aranja', function() { 5993 var search = new Search(); 5994 search.init(); 5995 }); 5996})(jQuery, metadata); 5997 5998window.dacStore = (function(window) { 5999 /** 6000 * Creates a new persistent store. 6001 * If localStorage is unavailable, the items are stored in memory. 6002 * 6003 * @constructor 6004 * @param {string} name The name of the store 6005 * @param {number} maxSize The maximum number of items the store can hold. 6006 */ 6007 var Store = function(name, maxSize) { 6008 var content = []; 6009 6010 var hasLocalStorage = !!window.localStorage; 6011 6012 if (hasLocalStorage) { 6013 try { 6014 content = JSON.parse(window.localStorage.getItem(name) || []); 6015 } catch (e) { 6016 // Store contains invalid data 6017 window.localStorage.removeItem(name); 6018 } 6019 } 6020 6021 function push(item) { 6022 if (content[0] === item) { 6023 return; 6024 } 6025 6026 content.unshift(item); 6027 6028 if (maxSize) { 6029 content.splice(maxSize, content.length); 6030 } 6031 6032 if (hasLocalStorage) { 6033 window.localStorage.setItem(name, JSON.stringify(content)); 6034 } 6035 } 6036 6037 function all() { 6038 // Return a copy 6039 return content.slice(); 6040 } 6041 6042 return { 6043 push: push, 6044 all: all 6045 }; 6046 }; 6047 6048 var stores = { 6049 'search-history': new Store('search-history', 3) 6050 }; 6051 6052 /** 6053 * Get a named persistent store. 6054 * @param {string} name 6055 * @return {Store} 6056 */ 6057 return function getStore(name) { 6058 return stores[name]; 6059 }; 6060})(window); 6061 6062(function($) { 6063 'use strict'; 6064 6065 /** 6066 * A component that swaps two dynamic height views with an animation. 6067 * Listens for the following events: 6068 * * swap-content: triggers SwapContent.swap_() 6069 * * swap-reset: triggers SwapContent.reset() 6070 * @param el 6071 * @param options 6072 * @constructor 6073 */ 6074 function SwapContent(el, options) { 6075 this.el = $(el); 6076 this.options = $.extend({}, SwapContent.DEFAULTS_, options); 6077 this.options.dynamic = this.options.dynamic === 'true'; 6078 this.containers = this.el.find(this.options.container); 6079 this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0); 6080 this.el.on('swap-content', this.swap.bind(this)); 6081 this.el.on('swap-reset', this.reset.bind(this)); 6082 this.el.find(this.options.swapButton).on('click keypress', function(e) { 6083 if (e.type == 'keypress' && e.which == 13 || e.type == 'click') { 6084 this.swap(); 6085 } 6086 }.bind(this)); 6087 } 6088 6089 /** 6090 * SwapContent's default settings. 6091 * @type {{activeClass: string, container: string, transitionSpeed: number}} 6092 * @private 6093 */ 6094 SwapContent.DEFAULTS_ = { 6095 activeClass: 'dac-active', 6096 container: '[data-swap-container]', 6097 dynamic: 'true', 6098 swapButton: '[data-swap-button]', 6099 transitionSpeed: 500 6100 }; 6101 6102 /** 6103 * Returns container's visible height. 6104 * @param container 6105 * @returns {number} 6106 */ 6107 SwapContent.prototype.currentHeight = function(container) { 6108 return container.children('.' + this.options.activeClass).outerHeight(); 6109 }; 6110 6111 /** 6112 * Reset to show initial content 6113 */ 6114 SwapContent.prototype.reset = function() { 6115 if (!this.initiallyActive.hasClass(this.initiallyActive)) { 6116 this.containers.children().toggleClass(this.options.activeClass); 6117 } 6118 }; 6119 6120 /** 6121 * Complete the swap. 6122 */ 6123 SwapContent.prototype.complete = function() { 6124 this.containers.height('auto'); 6125 this.containers.trigger('swap-complete'); 6126 }; 6127 6128 /** 6129 * Perform the swap of content. 6130 */ 6131 SwapContent.prototype.swap = function() { 6132 this.containers.each(function(index, container) { 6133 container = $(container); 6134 6135 if (!this.options.dynamic) { 6136 container.children().toggleClass(this.options.activeClass); 6137 this.complete.bind(this); 6138 $('.' + this.options.activeClass).focus(); 6139 return; 6140 } 6141 6142 container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass); 6143 container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed, 6144 this.complete.bind(this)); 6145 }.bind(this)); 6146 }; 6147 6148 /** 6149 * jQuery plugin 6150 * @param {object} options - Override default options. 6151 */ 6152 $.fn.dacSwapContent = function(options) { 6153 return this.each(function() { 6154 new SwapContent(this, options); 6155 }); 6156 }; 6157 6158 /** 6159 * Data Attribute API 6160 */ 6161 $(document).on('ready.aranja', function() { 6162 $('[data-swap]').each(function() { 6163 $(this).dacSwapContent($(this).data()); 6164 }); 6165 }); 6166})(jQuery); 6167 6168/* Tabs */ 6169(function($) { 6170 'use strict'; 6171 6172 /** 6173 * @param {HTMLElement} el - The DOM element. 6174 * @param {Object} options 6175 * @constructor 6176 */ 6177 function Tabs(el, options) { 6178 this.el = $(el); 6179 this.options = $.extend({}, Tabs.DEFAULTS_, options); 6180 this.init(); 6181 } 6182 6183 Tabs.DEFAULTS_ = { 6184 activeClass: 'dac-active', 6185 viewDataAttr: 'tab-view', 6186 itemDataAttr: 'tab-item' 6187 }; 6188 6189 Tabs.prototype.init = function() { 6190 var itemDataAttribute = '[data-' + this.options.itemDataAttr + ']'; 6191 this.tabEl_ = this.el.find(itemDataAttribute); 6192 this.tabViewEl_ = this.el.find('[data-' + this.options.viewDataAttr + ']'); 6193 this.el.on('click.dac-tabs', itemDataAttribute, this.changeTabs.bind(this)); 6194 }; 6195 6196 Tabs.prototype.changeTabs = function(event) { 6197 var current = $(event.currentTarget); 6198 var index = current.index(); 6199 6200 if (current.hasClass(this.options.activeClass)) { 6201 current.add(this.tabViewEl_.eq(index)).removeClass(this.options.activeClass); 6202 } else { 6203 this.tabEl_.add(this.tabViewEl_).removeClass(this.options.activeClass); 6204 current.add(this.tabViewEl_.eq(index)).addClass(this.options.activeClass); 6205 } 6206 }; 6207 6208 /** 6209 * jQuery plugin 6210 */ 6211 $.fn.dacTabs = function() { 6212 return this.each(function() { 6213 var el = $(this); 6214 new Tabs(el, el.data()); 6215 }); 6216 }; 6217 6218 /** 6219 * Data Attribute API 6220 */ 6221 $(function() { 6222 $('[data-tabs]').dacTabs(); 6223 }); 6224})(jQuery); 6225 6226/* Toast Component */ 6227(function($) { 6228 'use strict'; 6229 /** 6230 * @constant 6231 * @type {String} 6232 */ 6233 var LOCAL_STORAGE_KEY = 'toast-closed-index'; 6234 6235 /** 6236 * Dictionary from local storage. 6237 */ 6238 var toastDictionary = localStorage.getItem(LOCAL_STORAGE_KEY); 6239 toastDictionary = toastDictionary ? JSON.parse(toastDictionary) : {}; 6240 6241 /** 6242 * Variable used for caching the body. 6243 */ 6244 var bodyCached; 6245 6246 /** 6247 * @param {HTMLElement} el - The DOM element. 6248 * @param {Object} options 6249 * @constructor 6250 */ 6251 function Toast(el, options) { 6252 this.el = $(el); 6253 this.options = $.extend({}, Toast.DEFAULTS_, options); 6254 this.init(); 6255 } 6256 6257 Toast.DEFAULTS_ = { 6258 closeBtnClass: 'dac-toast-close-btn', 6259 closeDuration: 200, 6260 visibleClass: 'dac-visible', 6261 wrapClass: 'dac-toast-wrap' 6262 }; 6263 6264 /** 6265 * Generate a close button. 6266 * @returns {*|HTMLElement} 6267 */ 6268 Toast.prototype.closeBtn = function() { 6269 this.closeBtnEl = this.closeBtnEl || $('<button class="' + this.options.closeBtnClass + '">' + 6270 '<span class="dac-button dac-raised dac-primary">OK</span>' + 6271 '</button>'); 6272 return this.closeBtnEl; 6273 }; 6274 6275 /** 6276 * Initialize a new toast element 6277 */ 6278 Toast.prototype.init = function() { 6279 this.hash = this.el.text().replace(/[\s\n\t]/g, '').split('').slice(0, 128).join(''); 6280 6281 if (toastDictionary[this.hash]) { 6282 return; 6283 } 6284 6285 this.closeBtn().on('click', this.onClickHandler.bind(this)); 6286 this.el.find('.' + this.options.wrapClass).append(this.closeBtn()); 6287 this.el.addClass(this.options.visibleClass); 6288 this.dynamicPadding(this.el.outerHeight()); 6289 }; 6290 6291 /** 6292 * Add padding to make sure all page is visible. 6293 */ 6294 Toast.prototype.dynamicPadding = function(val) { 6295 var currentPadding = parseInt(bodyCached.css('padding-bottom') || 0); 6296 bodyCached.css('padding-bottom', val + currentPadding); 6297 }; 6298 6299 /** 6300 * Remove a toast from the DOM 6301 */ 6302 Toast.prototype.remove = function() { 6303 this.dynamicPadding(-this.el.outerHeight()); 6304 this.el.remove(); 6305 }; 6306 6307 /** 6308 * Handle removal of the toast. 6309 */ 6310 Toast.prototype.onClickHandler = function() { 6311 // Only fadeout toasts from top of stack. Others are removed immediately. 6312 var duration = this.el.index() === 0 ? this.options.closeDuration : 0; 6313 this.el.fadeOut(duration, this.remove.bind(this)); 6314 6315 // Save closed state. 6316 toastDictionary[this.hash] = 1; 6317 localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(toastDictionary)); 6318 }; 6319 6320 /** 6321 * jQuery plugin 6322 * @param {object} options - Override default options. 6323 */ 6324 $.fn.dacToast = function() { 6325 return this.each(function() { 6326 var el = $(this); 6327 new Toast(el, el.data()); 6328 }); 6329 }; 6330 6331 /** 6332 * Data Attribute API 6333 */ 6334 $(function() { 6335 bodyCached = $('#body-content'); 6336 $('[data-toast]').dacToast(); 6337 }); 6338})(jQuery); 6339 6340(function($) { 6341 function Toggle(el) { 6342 $(el).on('click.dac.togglesection', this.toggle); 6343 } 6344 6345 Toggle.prototype.toggle = function() { 6346 var $this = $(this); 6347 6348 var $parent = getParent($this); 6349 var isExpanded = $parent.hasClass('is-expanded'); 6350 6351 transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded); 6352 $parent.toggleClass('is-expanded'); 6353 6354 return false; 6355 }; 6356 6357 function getParent($this) { 6358 var selector = $this.attr('data-target'); 6359 6360 if (!selector) { 6361 selector = $this.attr('href'); 6362 selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, ''); 6363 } 6364 6365 var $parent = selector && $(selector); 6366 6367 $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle'); 6368 6369 return $parent.length ? $parent : $this.parent(); 6370 } 6371 6372 /** 6373 * Runs a transition of max-height along with responsive styles which hide or expand the element. 6374 * @param $el 6375 * @param visible 6376 */ 6377 function transitionMaxHeight($el, visible) { 6378 var contentHeight = $el.prop('scrollHeight'); 6379 var targetHeight = visible ? contentHeight : 0; 6380 var duration = $el.transitionDuration(); 6381 6382 // If we're hiding, first set the maxHeight we're transitioning from. 6383 if (!visible) { 6384 $el.css({ 6385 transitionDuration: '0s', 6386 maxHeight: contentHeight + 'px' 6387 }) 6388 .resolveStyles() 6389 .css('transitionDuration', ''); 6390 } 6391 6392 // Transition to new state 6393 $el.css('maxHeight', targetHeight); 6394 6395 // Reset maxHeight to css value after transition. 6396 setTimeout(function() { 6397 $el.css({ 6398 transitionDuration: '0s', 6399 maxHeight: '' 6400 }) 6401 .resolveStyles() 6402 .css('transitionDuration', ''); 6403 }, duration); 6404 } 6405 6406 // Utility to get the transition duration for the element. 6407 $.fn.transitionDuration = function() { 6408 var d = $(this).css('transitionDuration') || '0s'; 6409 6410 return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0); 6411 }; 6412 6413 // jQuery plugin 6414 $.fn.toggleSection = function(option) { 6415 return this.each(function() { 6416 var $this = $(this); 6417 var data = $this.data('dac.togglesection'); 6418 if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));} 6419 if (typeof option === 'string') {data[option].call($this);} 6420 }); 6421 }; 6422 6423 // Data api 6424 $(document) 6425 .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle); 6426})(jQuery); 6427 6428(function(window) { 6429 /** 6430 * Media query breakpoints. Should match CSS. 6431 */ 6432 var BREAKPOINTS = { 6433 mobile: [0, 719], 6434 tablet: [720, 959], 6435 desktop: [960, 9999] 6436 }; 6437 6438 /** 6439 * Fisher-Yates Shuffle (Knuth shuffle). 6440 * @param {Array} input 6441 * @returns {Array} shuffled array. 6442 */ 6443 function shuffle(input) { 6444 for (var i = input.length; i >= 0; i--) { 6445 var randomIndex = Math.floor(Math.random() * (i + 1)); 6446 var randomItem = input[randomIndex]; 6447 input[randomIndex] = input[i]; 6448 input[i] = randomItem; 6449 } 6450 6451 return input; 6452 } 6453 6454 /** 6455 * Matches media breakpoints like in CSS. 6456 * @param {string} form of either mobile, tablet or desktop. 6457 */ 6458 function matchesMedia(form) { 6459 var breakpoint = BREAKPOINTS[form]; 6460 return window.innerWidth >= breakpoint[0] && window.innerWidth <= breakpoint[1]; 6461 } 6462 6463 window.util = { 6464 shuffle: shuffle, 6465 matchesMedia: matchesMedia 6466 }; 6467})(window); 6468 6469(function($, window) { 6470 'use strict'; 6471 6472 var YouTubePlayer = (function() { 6473 var player; 6474 6475 function VideoPlayer() { 6476 this.mPlayerPaused = false; 6477 this.doneSetup = false; 6478 } 6479 6480 VideoPlayer.prototype.setup = function() { 6481 // loads the IFrame Player API code asynchronously. 6482 $.getScript('https://www.youtube.com/iframe_api'); 6483 6484 // Add the shadowbox HTML to the body 6485 $('body').prepend( 6486'<div id="video-player" class="Video">' + 6487 '<div id="video-overlay" class="Video-overlay" />' + 6488 '<div class="Video-container">' + 6489 '<div class="Video-frame">' + 6490 '<span class="Video-loading">Loading…</span>' + 6491 '<div id="youTubePlayer"></div>' + 6492 '</div>' + 6493 '<div class="Video-controls">' + 6494 '<button id="picture-in-picture" class="Video-button Video-button--picture-in-picture">' + 6495 '<button id="close-video" class="Video-button Video-button--close" />' + 6496 '</div>' + 6497 '</div>' + 6498'</div>'); 6499 6500 this.videoPlayer = $('#video-player'); 6501 6502 var pictureInPictureButton = this.videoPlayer.find('#picture-in-picture'); 6503 pictureInPictureButton.on('click.aranja', this.toggleMinimizeVideo.bind(this)); 6504 6505 var videoOverlay = this.videoPlayer.find('#video-overlay'); 6506 var closeButton = this.videoPlayer.find('#close-video'); 6507 var closeVideo = this.closeVideo.bind(this); 6508 videoOverlay.on('click.aranja', closeVideo); 6509 closeButton.on('click.aranja', closeVideo); 6510 6511 this.doneSetup = true; 6512 }; 6513 6514 VideoPlayer.prototype.startYouTubePlayer = function(videoId) { 6515 this.videoPlayer.show(); 6516 6517 if (!this.isLoaded) { 6518 this.queueVideo = videoId; 6519 return; 6520 } 6521 6522 this.mPlayerPaused = false; 6523 // check if we've already created this player 6524 if (!this.youTubePlayer) { 6525 // check if there's a start time specified 6526 var idAndHash = videoId.split('#'); 6527 var startTime = 0; 6528 if (idAndHash.length > 1) { 6529 startTime = idAndHash[1].split('t=')[1] !== undefined ? idAndHash[1].split('t=')[1] : 0; 6530 } 6531 // enable localized player 6532 var lang = getLangPref(); 6533 var captionsOn = lang === 'en' ? 0 : 1; 6534 6535 this.youTubePlayer = new YT.Player('youTubePlayer', { 6536 height: 720, 6537 width: 1280, 6538 videoId: idAndHash[0], 6539 // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 6540 playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn}, 6541 // jscs:enable 6542 events: { 6543 'onReady': this.onPlayerReady.bind(this), 6544 'onStateChange': this.onPlayerStateChange.bind(this) 6545 } 6546 }); 6547 } else { 6548 // if a video different from the one already playing was requested, cue it up 6549 if (videoId !== this.getVideoId()) { 6550 this.youTubePlayer.cueVideoById(videoId); 6551 } 6552 this.youTubePlayer.playVideo(); 6553 } 6554 }; 6555 6556 VideoPlayer.prototype.onPlayerReady = function(event) { 6557 if (!isMobile) { 6558 event.target.playVideo(); 6559 this.mPlayerPaused = false; 6560 } 6561 }; 6562 6563 VideoPlayer.prototype.toggleMinimizeVideo = function(event) { 6564 event.stopPropagation(); 6565 this.videoPlayer.toggleClass('Video--picture-in-picture'); 6566 }; 6567 6568 VideoPlayer.prototype.closeVideo = function() { 6569 try { 6570 this.youTubePlayer.pauseVideo(); 6571 } catch (e) { 6572 } 6573 this.videoPlayer.fadeOut(200, function() { 6574 this.videoPlayer.removeClass('Video--picture-in-picture'); 6575 }.bind(this)); 6576 }; 6577 6578 VideoPlayer.prototype.getVideoId = function() { 6579 // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 6580 return this.youTubePlayer && this.youTubePlayer.getVideoData().video_id; 6581 // jscs:enable 6582 }; 6583 6584 /* Track youtube playback for analytics */ 6585 VideoPlayer.prototype.onPlayerStateChange = function(event) { 6586 var videoId = this.getVideoId(); 6587 var currentTime = this.youTubePlayer && this.youTubePlayer.getCurrentTime(); 6588 6589 // Video starts, send the video ID 6590 if (event.data === YT.PlayerState.PLAYING) { 6591 if (this.mPlayerPaused) { 6592 devsite.analytics.trackAnalyticsEvent('event', 6593 'Videos', 'Resume', videoId); 6594 } else { 6595 // track the start playing event so we know from which page the video was selected 6596 devsite.analytics.trackAnalyticsEvent('event', 6597 'Videos', 'Start: ' + videoId, 'on: ' + document.location.href); 6598 } 6599 this.mPlayerPaused = false; 6600 } 6601 6602 // Video paused, send video ID and video elapsed time 6603 if (event.data === YT.PlayerState.PAUSED) { 6604 devsite.analytics.trackAnalyticsEvent('event', 6605 'Videos', 'Paused: ' + videoId, 'on: ' + currentTime); 6606 this.mPlayerPaused = true; 6607 } 6608 6609 // Video finished, send video ID and video elapsed time 6610 if (event.data === YT.PlayerState.ENDED) { 6611 devsite.analytics.trackAnalyticsEvent('event', 6612 'Videos', 'Finished: ' + videoId, 'on: ' + currentTime); 6613 this.mPlayerPaused = true; 6614 } 6615 }; 6616 6617 return { 6618 getPlayer: function() { 6619 if (!player) { 6620 player = new VideoPlayer(); 6621 } 6622 6623 return player; 6624 } 6625 }; 6626 })(); 6627 6628 var videoPlayer = YouTubePlayer.getPlayer(); 6629 6630 window.onYouTubeIframeAPIReady = function() { 6631 videoPlayer.isLoaded = true; 6632 6633 if (videoPlayer.queueVideo) { 6634 videoPlayer.startYouTubePlayer(videoPlayer.queueVideo); 6635 } 6636 }; 6637 6638 function wrapLinkInPlayer(e) { 6639 e.preventDefault(); 6640 6641 if (!videoPlayer.doneSetup) { 6642 videoPlayer.setup(); 6643 } 6644 6645 var videoIdMatches = $(e.currentTarget).attr('href').match(/(?:youtu.be\/|v=)([^&]*)/); 6646 var videoId = videoIdMatches && videoIdMatches[1]; 6647 6648 if (videoId) { 6649 videoPlayer.startYouTubePlayer(videoId); 6650 } 6651 } 6652 6653 $(document).on('click.video', 'a[href*="youtube.com/watch"], a[href*="youtu.be"]', wrapLinkInPlayer); 6654})(jQuery, window); 6655 6656/** 6657 * Wide table 6658 * 6659 * Wraps tables in a scrollable area so you can read them on mobile. 6660 */ 6661(function($) { 6662 function initWideTable() { 6663 $('table.jd-sumtable').each(function(i, table) { 6664 $(table).wrap('<div class="dac-expand wide-table">'); 6665 }); 6666 } 6667 6668 $(function() { 6669 initWideTable(); 6670 }); 6671})(jQuery); 6672 6673/** Utilities */ 6674 6675/* returns the given string with all HTML brackets converted to entities 6676 TODO: move this to the site's JS library */ 6677function escapeHTML(string) { 6678 return string.replace(/</g,"<") 6679 .replace(/>/g,">"); 6680}; 6681 6682function getQueryVariable(variable) { 6683 var query = window.location.search.substring(1); 6684 var vars = query.split("&"); 6685 for (var i=0;i<vars.length;i++) { 6686 var pair = vars[i].split("="); 6687 if(pair[0] == variable){return pair[1];} 6688 } 6689 return(false); 6690}; 6691