1// ======== utility functions ======== 2 3$(document).ready(function() { 4 // javascript canary for signup forms - SIGNUP FORMS DON'T VALIDATE WITHOUT THIS 5 $("input[name='js_enabled'], input[name='javascript_enabled']").val('1'); // prove that javascript is enabled 6 7 // ======== MOBILE MENU ======= 8 var mobileMenu = $('.mobile-menu'); 9 10 // Mobile nav search has no submit button; user submits via Enter. 11 // Clear button is only visible when the input has text. 12 function updateMobileSearchClearVisibility() { 13 var $input = $('#search-mobile'); 14 var $clear = $('.js-mm-search-clear'); 15 if (!$input.length || !$clear.length) { 16 return; 17 } 18 var hasText = String($input.val() || '').trim().length > 0; 19 if (hasText) { 20 $clear.removeAttr('hidden'); 21 } else { 22 $clear.attr('hidden', 'hidden'); 23 } 24 } 25 26 function openMobileMenu() { 27 mobileMenu.addClass('open').animate({ right: '0px' }, 150); 28 $('.overlay').fadeIn(150); 29 $('body').addClass('no-scroll'); 30 updateMobileSearchClearVisibility(); 31 } 32 33 function closeMobileMenu() { 34 mobileMenu.removeClass('open').animate({ right: '-1000px' }, 150); 35 $('.overlay').fadeOut(150); 36 $('body').removeClass('no-scroll'); 37 $('.mm-accordion ul').slideUp(); 38 $('.mm-accordion button').removeClass('active'); 39 } 40 41 $('.mobile-menu-toggle').on('click', function(event) { 42 event.preventDefault(); 43 if (mobileMenu.hasClass('open')) { 44 closeMobileMenu(); 45 } else { 46 openMobileMenu(); 47 } 48 }); 49 50 $(document).on('click', '.js-mm-close', function(event) { 51 event.preventDefault(); 52 event.stopPropagation(); 53 closeMobileMenu(); 54 }); 55 56 $('.overlay').on('click', closeMobileMenu); 57 58 $(document).on('input', '#search-mobile', updateMobileSearchClearVisibility); 59 $(document).on('click', '.js-mm-search-clear', function(event) { 60 event.preventDefault(); 61 event.stopPropagation(); 62 $('#search-mobile').val('').trigger('input'); 63 updateMobileSearchClearVisibility(); 64 $('#search-mobile').trigger('focus'); 65 }); 66 67 updateMobileSearchClearVisibility(); 68 69 // Blocks empty submits and limits each nav search form to one submission per page load. 70 // Each matched form gets its own `submitted` flag via closure. 71 // This is the same functionality as defined in assets/js/src/search/semantic_search.js 72 $('.mm-search__form, .menu-search-inner-wrapper form').each(function() { 73 var form = this; 74 var submitted = false; 75 $(form).on('submit', function(e) { 76 var input = form.elements.q; 77 var q = input ? String(input.value || '').trim() : ''; 78 if (submitted || !q) { 79 e.preventDefault(); 80 return; 81 } 82 submitted = true; 83 }); 84 }); 85 86 // topics accordion in mobile menu 87 $('.mm-accordion ul').hide(); 88 $('.mm-accordion button').click(function() { 89 $(this).next().slideToggle(); 90 $(this).toggleClass('active'); 91 }); 92 93 // ===== RIGHT SIDEBAR - PRIORITIZE BOXES ===== 94 function compressSidebar() { 95 // Keep trying to remove boxes for as long as the 96 // sidebar is taller than the content well 97 // Conveniently, height() will be undefined if either the 98 // sidebar or the container don't exist 99 // so the loop won't execute if the page is messed up or something. 100 while ($('.sidebar').height() >= $('#main-content').height()) { 101 var $boxes = $('.sidebar-box'); 102 // custom sort compare function to arrange boxes by priority attribute 103 $boxes.sort(function(a, b) { 104 var aPri = $(a).attr('data-box-priority'); 105 aPri = aPri ? aPri : 0; // priority assumed zero if not defined 106 var bPri = $(b).attr('data-box-priority'); 107 bPri = bPri ? bPri : 0; // priority assumed zero if not defined 108 if (aPri > bPri) { 109 return 1; 110 } else if (aPri < bPri) { 111 return -1; 112 } 113 // if (aPri === bPri) 114 return 0; 115 }); 116 // grab the last box, the one with the worst/largest priority score 117 var $removalCandidate = $boxes.last(); 118 if (!($removalCandidate) || !($removalCandidate.attr('data-box-priority') >
118 0)) { 119 // Bail if there are no boxes or if the candidate box has 120 // a priority of 0 (or less or undefined) 121 // which means never remove it. 122 // TODO: Refactor this so it doesn't use a break in a while loop 123 break; 124 } 125 $removalCandidate.remove(); 126 } 127 } 128 129 // run once when site DOM finishes loading 130 compressSidebar(); 131 // run again 1/2 second later to give stuff like ads more time to load 132 setTimeout(compressSidebar, 500); 133 134 // ======== DESKTOP DROPDOWN FUNCTIONALITY ======= 135 var menuBreakpoint = 928; // this is the pixel conversion of $menu-breakpoint (see _global_variables.scss) 136 var mediumBreakpoint = 1024; 137 138 /** 139 * Create a dropdown object 140 * @param { string }id - the class identifying the dropdown 141 * @param { bool } topics - True if this is the topics dropdown 142 * menuDropdownBar - the menu bar 143 * navItemWrapper - the wrapper around the list item in the nav 144 * navItemTrigger - the link for the list item in the nav 145 * Associate functions 146 * clickDropdown() - toggles the visibility of the menu bar, hides all other dropdowns when showing the clicked dropdown 147 * showDropdown() - show the dropdown menu bar by positioning it above the nav 148 * hideDropdown() - hide the dropdown menu bar 149 * showNavItem() - adds class to show the link in the nav 150 * hideNavItem() - removes class to hide the link in the nav 151 * @returns { void } 152 */ 153 var Dropdown = function(id, topics) { 154 this.topics = topics; 155 this.menuDropdownBar = $(id + '.menu-bar'); 156 this.navItemWrapper = $(id + '.top-nav-dropdown-item'); 157 this.navItemTrigger = $(id + '.top-nav-dropdown-item .nav-menu-item'); 158 }; 159 160 var clickDropdown = function(val) { 161 // nav slides down from menu on click 162 var clicked = navArray[val]; 163 clicked.navItemTrigger.on('click', function(event) { 164 event.preventDefault(); 165 if (search.hasClass('js-search-show')) { // if search is already shown, hide search 166 hideSearch(); 167 if (window.innerWidth >= mediumBreakpoint) { 168 // Check if user is scrolled up at or above where the site-menu normally appears 169 if ($(window).scrollTop() <= $('header').offset().top) { 170 showMenuBar(0); // topics 171 } 172 } 173 } 174 175 if (!clicked.menuDropdownBar.hasClass('js-menu-bar-hide')) { // if the clicked dropdown bar is already shown, hide it 176 hideMenuBar(val); 177 } else { // dropdown is not currently visible, hide all other visible dropdowns 178 for (var i = 0; i < navArray.length; i++) { 179 var item = navArray[i]; 180 // if a dropdown is showing (does not have class js-menu-bar-hide) 181 if (!item.menuDropdownBar.hasClass('js-menu-bar-hide')) { 182 // if the visible dropdown is topics, only hide if on medium screen or scrolled down on large 183 // this is only for topics because the topics bar should always be visible at the top of the page when on large screens 184 if (item.topics) { 185 if ( 186 (window.innerWidth < mediumBreakpoint) || 187 ($(window).scrollTop() > $('header').offset().top) 188 ) { // don't allow more than one dropdown to be open 189 hideMenuBar(i); 190 } 191 // if not the topics dropdown, hide it 192 } else { 193 hideMenuBar(i); 194 } 195 } 196 } 197 // show the clicked dropdown and add shadow at the bottom of it 198 navArray[val].menuDropdownBar.addClass('js-menu-bar-box-shadow'); 199 showMenuBar(val); 200 } 201 }); 202 }; 203 204 // create an array to hold all nav dropdown items 205 var navArray = []; 206 207 // always create the topics nav 208 var topics = new Dropdown('.topics', true); 209 navArray.push(topics); 210 211 // initialize all clickDropdown functions 212 for (var i = 0; i < navArray.length; i++) { 213 clickDropdown(i); 214 } 215 216 // Show the menu bar for a specific dropdown 217 var showMenuBar = function(index) { 218 navArray[index].menuDropdownBar.removeClass('js-menu-bar-hide'); // displays menu w top:55px 219 navArray[index].menuDropdownBar.addClass('js-menu-bar-transition'); 220 navArray[index].navItemWrapper.addClass('js-top-nav-item-active'); // toggles arrow 221 }; 222 223 // Hide the menu bar for a specific dropdown 224 var hideMenuBar = function(index) { 225 navArray[index].menuDropdownBar.addClass('js-menu-bar-hide'); // displays menu w top:0px behind menu 226 navArray[index].menuDropdownBar.removeClass('js-menu-bar-transition'); 227 navArray[index].menuDropdownBar.removeClass('js-menu-bar-box-shadow'); 228 navArray[index].navItemWrapper.removeClass('js-top-nav-item-active'); // toggles arrow 229 }; 230 231 // Show the nav item for a specific dropdown 232 var showNavItem = function(index) { 233 navArray[index].navItemWrapper.addClass('js-top-nav-item-visible'); 234 }; 235 236 // Hide the nav item for a specific dropdown 237 var hideNavItem = function(index) { 238 navArray[index].navItemWrapper.removeClass('js-top-nav-item-visible'); 239 }; 240 241 // search 242 var search = $('.menu-search'); 243 var siteMenu = $('.site-menu'); 244 // Header semantic search dropdown references/config. 245 var menuSearchElement = document.querySelector('.menu-search'); 246 var menuSearchForm = menuSearchElement ? menuSearchElement.querySelector('form') : null; 247 var menuSearchRelatedQuestionsList = menuSearchElement ? menuSearchElement.querySelector('.js-menu-search-related-questions') : null; 248 var menuSearchRelatedQuestionsUrl = menuSearchElement ? menuSearchElement.getAttribute('data-related-questions-url') : ''; 249 var menuSearchUrl = menuSearchElement 250 ? (menuSearchElement.getAttribute('data-search-url') || (menuSearchForm ? menuSearchForm.getAttribute('action') : '') || '/search/') 251 : '/search/'; 252 var menuSearchLoadingHtml = `<li class="menu-search-paa__item menu-search-paa__item--loading">${window.gettext('Loading questions...')}</li>`; 253 var menuSearchEmptyHtml = `<li class="menu-search-paa__item menu-search-paa__item--empty">${window.gettext('No related questions available right now.')}</li>`; 254 var menuSearchMaxQuestions = 5; 255 256 // Escape text before inserting into innerHTML. 257 function escapeHtml(value) { 258 var div = document.createElement('div'); 259 div.textContent = value || ''; 260 return div.innerHTML; 261 } 262 function buildMenuSearchQuestionHref(question) { 263 var separator = menuSearchUrl.indexOf('?') === -1 ? '?' : '&'; 264 return menuSearchUrl + separator + 'q=' + encodeURIComponent(question); 265 } 266 267 // Render up to 5 related question links in the dropdown. 268 function renderMenuSearchRelatedQuestions(items) { 269 if (!menuSearchRelatedQuestionsList) { 270 return; 271 } 272 var relatedItems = Array.isArray(items) ? items : []; 273 274 var questionItems = []; 275 for (var questionIndex = 0; questionIndex < relatedItems.length && questionItems.length < menuSearchMaxQuestions; questionIndex++) { 276 var row = relatedItems[questionIndex] || {}; 277 var question = String(row.question || '').trim(); 278 if (!question) { 279 continue; 280 } 281 questionItems.push( 282 '<li class="menu-search-paa__item">' + 283 '<a class="menu-search-paa__link" href="' + buildMenuSearchQuestionHref(question) + '">' + escapeHtml(question) + '</a>' + 284 '</li>' 285 ); 286 } 287 menuSearchRelatedQuestionsList.innerHTML = questionItems.length ? questionItems.join('') : menuSearchEmptyHtml; 288 } 289 290 // Fetch related questions for the header dropdown; fallback to empty state on error. 291 function loadMenuSearchRelatedQuestions() { 292 if (!menuSearchRelatedQuestionsList || !menuSearchRelatedQuestionsUrl) { 293 return; 294 } 295 menuSearchRelatedQuestionsList.innerHTML = menuSearchLoadingHtml; 296 297 fetch(menuSearchRelatedQuestionsUrl, { 298 method: 'GET', 299 headers: { 300 Accept: 'application/json', 301 }, 302 credentials: 'same-origin', 303 }) 304 .then(function(response) { 305 if (!response.ok) { 306 throw new Error(window.gettext('Failed to load related questions')); 307 } 308 return response.json(); 309 }) 310 .then(function(response) { 311 var items = (response && response.related_questions) || []; 312 // Filter to not-yet-seen questions (and mark the shown ones as seen) via the 313 // seenQuestionsTracker bundle, shared with the semantic search page. Falls back 314 // to showing unfiltered items if that bundle hasn't loaded for some reason. 315 if (typeof window.filterUnseenNavRelatedQuestions === 'function') { 316 items = window.filterUnseenNavRelatedQuestions(items); 317 } 318 renderMenuSearchRelatedQuestions(items); 319 }) 320 .catch(function() { 321 renderMenuSearchRelatedQuestions([]); 322 }); 323 } 324 function displaySearch() { 325 search.removeClass('js-search-hide'); 326 search.addClass('js-search-show'); 327 loadMenuSearchRelatedQuestions(); 328 $('#search-desktop').focus(); 329 $('.search-overlay').fadeIn(150); 330 $('body').addClass('no-scroll'); 331 } 332 function hideSearch() { 333 search.addClass('js-search-hide'); 334 search.removeClass('js-search-show'); 335 $('.search-overlay').fadeOut(150); 336 $('body').removeClass('no-scroll'); 337 } 338 function searchFixed() { 339 search.css('position', 'fixed'); 340 } 341 function searchUnfixed() { 342 search.css('position', 'absolute'); 343 } 344 function menuFixed() { 345 siteMenu.addClass('js-site-menu-fixed'); 346 for (i = 0; i < navArray.length; i++) { 347 navArray[i].menuDropdownBar.css('position', 'fixed'); 348 } 349 } 350 function menuUnfixed() { 351 siteMenu.removeClass('js-site-menu-fixed'); 352 for (i = 0; i < navArray.length; i++) { 353 navArray[i].menuDropdownBar.css('position', 'absolute'); 354 } 355 } 356
357 function topicsBarClosedByDefault() { 358 // This function checks if the current page is one of the pages 359 // where Topics bar should be closed by default (TECH-11050). 360 // Pages with this functionality are: 361 // - "Contact Us" page 362 // - Press release details pages 363 return ( 364 (window.location.pathname === '/contact/') || 365 (/^\/press-release\/[0-9]{8}-*/.test(window.location.pathname)) || 366 (/^\/press-release\/report\/v2\/21\//.test(window.location.pathname)) || 367 (/^\/press-release\/report\/v2\/7\//.test(window.location.pathname)) 368 ); 369 } 370 371 function updateMenuAppearance() { 372 // This function gets called *whenever* the page is resized or scrolled 373 // The rule is that the .site-menu bar is always fixed at the top, except when 374 // you're way up at the top of a desktop page and there's a leaderboard ad 375 // above the bar. In the logic below we actually fudge a little and the bar is 376 // always unfixed at the very top of the page, but it doesn't matter then anyway. 377 for (i = 0; i < navArray.length; i++) { 378 var item = navArray[i]; 379 if (!item.topics) { 380 // permanently show all dropdowns that are not topics 381 showNavItem(i); 382 } 383 } 384 if ($('header').length) { 385 if ($(window).scrollTop() <= $('header').offset().top) { 386 menuUnfixed(); 387 searchUnfixed(); 388 if ((window.innerWidth < mediumBreakpoint) && (window.innerWidth >= menuBreakpoint)) { 389 showNavItem(0); // topics link displayed 390 // hide all dropdowns 391 for (i = 0; i < navArray.length; i++) { 392 hideMenuBar(i); 393 } 394 } else if (!topicsBarClosedByDefault()) { 395 hideNavItem(0); // topics link hidden 396 showMenuBar(0); // topics 397 } 398 } else { 399 menuFixed(); 400 searchFixed(); 401 showNavItem(0); // topics link displayed 402 // hide all dropdowns 403 for (i = 0; i < navArray.length; i++) { 404 hideMenuBar(i); 405 } 406 if (window.innerWidth >= menuBreakpoint) { 407 // makes it so topics don't flash when page is loading or resizing 408 navArray[0].menuDropdownBar.show(); 409 } 410 } 411 } 412 // this positions the dropdown bars relative to the trigger's position in the menu 413 for (i = 0; i < navArray.length; i++) { 414 if (navArray[i].menuDropdownBar.length !== 0) { 415 if ((window.innerWidth < mediumBreakpoint) && (window.innerWidth >= menuBreakpoint)) { 416 // position dropdown menus on medium 417 navArray[i].menuDropdownBar.css('left', (parseInt(navArray[i].navItemWrapper.offset().left, 10) + 'px')); 418 } else if (window.innerWidth >= mediumBreakpoint) { 419 // dropdown bars positioned on the left if at large screen sizes 420 navArray[i].menuDropdownBar.css('left', '0px'); 421 } 422 } 423 } 424 } 425 426 $(document).ready(updateMenuAppearance); // run once when page loads 427 $(window).scroll(updateMenuAppearance); // run every time user scrolls 428 $(window).resize(updateMenuAppearance); // run every time the browser is resized 429 430 // controls search open and close behavior with dropdown bars 431 $('.search-toggle, .menu-search .close, .search-overlay').on('click', function(event) { 432 event.preventDefault(); 433 // if search is displayed, hide search 434 if (search.hasClass('js-search-show')) { 435 hideSearch(); 436 // else, display search and hide/show appropriate dropdowns 437 } else { 438 // checks if the fixed class is not added and if user is on a large screen 439 // this class is added on scroll (ie. the user isn't at the top of the page) 440 if ((!siteMenu.hasClass('js-site-menu-fixed')) && (window.innerWidth > mediumBreakpoint)) { 441 showMenuBar(0); // show topics bar 442 // hide other dropdowns besides topics 443 for (i = 1; i < navArray.length; i++) { 444 hideMenuBar(i); // hide all open dropdowns 445 } 446 } else { 447 // hide all open dropdowns if not at the top 448 for (i = 0; i < navArray.length; i++) { 449 hideMenuBar(i); // hide all open dropdowns 450 } 451 } 452 displaySearch(); 453 } 454 }); 455 456 // close menu search when "x" is clicked 457 $('.menu-search .close').on('click', function() { 458 hideSearch(); 459 }); 460 461 if (window.location.pathname === '/dashboard/admin/') { 462 $(document).ready(function() { 463 menuUnfixed(); 464 showMenuBar(0); // topics 465 hideNavItem(0); // topics 466 }); 467 468 $(window).scroll(function() { 469 menuUnfixed(); 470 showMenuBar(0); // topics 471 hideNavItem(0); // topics 472 }); 473 474 $(window).resize(function() { 475 menuUnfixed(); 476 showMenuBar(0); // topics 477 hideNavItem(0); // topics 478 }); 479 } 480 481 if (topicsBarClosedByDefault()) { 482 $(document).ready(function() { 483 hideMenuBar(0); // topics 484 showNavItem(0); // topics 485 }); 486 } 487 488 // SIGNUP INPUT CLICK DROPDOWN 489 // when you click on the form input to type in your email, 490 // the checkbox list of daily and weekly publications drops down 491 $('.signup .js-email-input input, .inline-signup .js-email-input input').on('click, focus', function() { 492 var thisCheckboxList = $(this).parents('form').find('.signup-list'); // only target this specific checkbox list 493 var checkboxListChildren = thisCheckboxList.children(); // get children of this ul 494 var numberOfCheckboxes = checkboxListChildren.length; // get number of checkboxes 495 if (numberOfCheckboxes > 0) { // drop down list when clicking input 496 thisCheckboxList.slideDown('500'); 497 } 498 }); 499 500 // show screen reader text for browsers that don't support placeholder text 501 if (!('placeholder' in document.createElement('input'))) { 502 $('span.screen-reader-text').removeClass('screen-reader-text').addClass('screen-reader-text-visible'); 503 } 504 505 // close messages 506 // uses .closest in case there are two messages on a page 507 $('.message__close').click(function() { 508 var thisMessage = $(this).closest('.message'); 509 $(thisMessage).fadeOut('fast'); 510 }); 511 512 // close site notification alert and set a cookie on this particular notification 513 // note that we expect here for the notification close div to have attributes 514 // data-notification-id and data-notification-expires 515 // use these to construct the cookie definition here 516 $('.site-alert__close').click(function() { 517 var cookieString = '_id_site_notification=' + $(this).attr('data-notification-id') + 518 ';expires=' + $(this).attr('data-notification-expires') + ';path=/'; 519 document.cookie = cookieString; 520 $('.site-alert').hide(); 521 }); 522 523 // accordion for search after first page 524 if ($('body').hasClass('not-first-page')) { 525 $('.js-search-accordion .js-search-accordion-inner').hide(); 526 $('.js-search-accordion .js-search-accordion-toggle').click(function() { 527 $('.js-search-accordion .js-search-accordion-inner').slideToggle(); 528 $(this).toggleClass('js-search-active'); // rotates arrow 529 }); 530 } else { 531 $('div.js-search-accordion').removeClass('js-search-accordion'); 532 } 533 534 // Function to show clear icon when text typed into input 535 function clearIcon(inputBox) { 536 var inputWrapper = $(inputBox).closest('.search-input'); 537 var inputClear = $(inputWrapper).find('.search-input__clear'); 538 var inputValue = $(inputBox).val(); 539 if (inputValue !== null && inputValue !== '') { 540 $(inputClear).show(); 541 } else { 542 $(inputClear).hide(); 543 } 544 } 545 546 // clears text in input on click of x 547 $('.search-input__clear').click( function() { 548 $(this).parent().find('input').val(''); 549 $(this).hide(); 550 }); 551 552 // shows clear 'x' on page load if text in input 553 clearIcon($('.search-input').find('input')); 554 // shows clear 'x' only when input is not empty 555 $('.search-input').find('input').keyup(function() { 556 clearIcon($(this)); 557 }); 558 559 // Remove sidebar boxes from the poll response page 560 if (window.location.pathname === '/poll-response/') { 561 $('.sidebar-box').remove(); 562 } 563 564 // Set cookie if utm_medium param is used 565 const url = new URL(window.location.href); 566 const utmMedium = url.searchParams.get('utm_medium'); 567 if (utmMedium) { 568 document.cookie = 'utmMedium=' + encodeURIComponent(utmMedium) + '; path=/; domain=' + window.location.hostname; 569 } 570});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.