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