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