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