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