PageSourceSearch

https://www.wastedive.com/static/js/app.js?520919230926

js wastedive.com collected 2026-09-24 08:55:44 UTC 22,057 bytes, 570 lines download raw bytes

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.