PageSourceSearch

https://pointsoflight.org/wp-content/themes/points-of-light/assets/js/news.js?ver=1789989442

js pointsoflight.org collected 2026-09-24 08:51:42 UTC 20,672 bytes, 674 lines download raw bytes

1"use strict";
2
3(function ($, win, doc) {
4  if (!$) return;
5
6  var config = win.polNews || {};
7  var ajaxUrl = config.ajaxUrl || "";
8  var ajaxNonce = config.nonce || "";
9  var windowSize = parseInt(config.windowSize, 10) || 4;
10  var windowStep = parseInt(config.windowStep, 10) || 2;
11  var i18n = config.i18n || {};
12  var instanceCount = 0;
13  var reduceMotion =
14    typeof win.matchMedia === "function" &&
15    win.matchMedia("(prefers-reduced-motion: reduce)").matches;
16  var filterDuration = reduceMotion ? 0 : 260;
17  var filterKeys = ["news_year"];
18  // WordPress reserves `page` on static pages and strips it via canonical redirect.
19  var urlPageParam = "news_page";
20
21  function getTopPostIds($root) {
22    var raw = $root.attr("data-top-post-ids") || "[]";
23    try {
24      var ids = JSON.parse(raw);
25      if (!Array.isArray(ids)) return [];
26      return ids
27        .map(function (id) {
28          return parseInt(id, 10);
29        })
30        .filter(function (id) {
31          return id > 0;
32        });
33    } catch (error) {
34      return [];
35    }
36  }
37
38  function getSelectedFilters($root) {
39    var filters = {};
40    filterKeys.forEach(function (key) {
41      filters[key] = [];
42    });
43
44    $root.find(".js-news-filter-checkbox:checked").each(function () {
45      var $input = $(this);
46      if ($input.hasClass("js-news-filter-all")) return;
47      var $filter = $input.closest(".js-news-filter");
48      var key = $filter.data("filter");
49      var year = String($input.val() || "");
50      if (!key || !year || year === "all") return;
51      if (!filters[key]) filters[key] = [];
52      filters[key].push(year);
53    });
54
55    return filters;
56  }
57
58  function buildAjaxBody(filters, page, topPostIds) {
59    var body = new FormData();
60    body.append("action", "pol_news_query");
61    body.append("nonce", ajaxNonce);
62    body.append("page", String(page || 1));
63    (topPostIds || []).forEach(function (id) {
64      body.append("top_post_ids[]", String(id));
65    });
66    filterKeys.forEach(function (key) {
67      (filters[key] || []).forEach(function (year) {
68        body.append(key + "[]", year);
69      });
70    });
71    return body;
72  }
73
74  function getPageFromUrl() {
75    var params = new URLSearchParams(win.location.search);
76    var page = parseInt(params.get(urlPageParam), 10);
77    return page > 0 ? page : 1;
78  }
79
80  function getFiltersFromUrl() {
81    var params = new URLSearchParams(win.location.search);
82    var filters = {};
83    filterKeys.forEach(function (key) {
84      filters[key] = params.getAll(key).filter(function (year) {
85        return year !== "";
86      });
87    });
88    return filters;
89  }
90
91  function buildBrowserQuery(filters, page) {
92    var params = new URLSearchParams();
93    var currentPage = page || 1;
94    if (currentPage > 1) {
95      params.set(urlPageParam, String(currentPage));
96    }
97    filterKeys.forEach(function (key) {
98      (filters[key] || []).forEach(function (year) {
99        params.append(key, year);
100      });
101    });
102    return params.toString();
103  }
104
105  function buildBrowserUrl(filters, page) {
106    var query = buildBrowserQuery(filters, page);
107    var url = win.location.pathname;
108    if (query) {
109      url += "?" + query;
110    }
111    if (win.location.hash) {
112      url += win.location.hash;
113    }
114    return url;
115  }
116
117  function updateBrowserUrl(filters, page, replace) {
118    if (!win.history || typeof win.history.pushState !== "function") {
119      return;
120    }
121    var url = buildBrowserUrl(filters, page);
122    if (replace) {
123      win.history.replaceState({ polNews: true }, "", url);
124      return;
125    }
126    win.history.pushState({ polNews: true }, "", url);
127  }
128
129  function setFiltersFromState($root, filters) {
130    $root.find(".js-news-filter-checkbox").each(function () {
131      var $input = $(this);
132      if ($input.hasClass("js-news-filter-all")) return;
133      var $filter = $input.closest(".js-news-filter");
134      var key = $filter.data("filter");
135      var year = String($input.val() || "");
136      var isSelected =
137        !!key &&
138        !!year &&
139        year !== "all" &&
140        (filters[key] || []).indexOf(year) !== -1;
141      $input.prop("checked", isSelected);
142      $input
143        .closest(".dropdown__option")
144        .attr("aria-selected", isSelected ? "true" : "false");
145    });
146  }
147
148  function syncStateFromUrl($root, shouldFetch, shouldScroll) {
149    var filters = getFiltersFromUrl();
150    var page = getPageFromUrl();
151    setFiltersFromState($root, filters);
152    $root.data("news-page", page);
153    if (!shouldScroll) {
154      $root.data("news-window-start", 1);
155    }
156    updateFilterLabels($root);
157    renderActiveTags($root);
158    if (shouldFetch) {
159      fetchNews($root, page);
160      if (shouldScroll) {
161        scrollToListing($root);
162      }
163    }
164  }
165
166  function closeFilter($filter) {
167    var $btn = $filter.find(".js-news-filter-btn");
168    var $menu = $filter.find(".js-news-filter-dropdown");
169    if ($btn.attr("aria-expanded") !== "true") return;
170    $btn.attr("aria-expanded", "false");
171    $menu.stop(true, true).slideUp(filterDuration, function () {
172      $menu.attr("hidden", "");
173    });
174  }
175
176  function openFilter($filter) {
177    var $btn = $filter.find(".js-news-filter-btn");
178    var $menu = $filter.find(".js-news-filter-dropdown");
179    $btn.attr("aria-expanded", "true");
180    $menu.removeAttr("hidden");
181    $menu.stop(true, true).slideDown(filterDuration);
182  }
183
184  function updateFilterLabels($root) {
185    $root.find(".js-news-filter").each(function () {
186      var $filter = $(this);
187      var $value = $filter.find(".js-news-filter-value");
188      var defaultLabel = $value.data("default") || "";
189      var labels = [];
190      $filter.find(".js-news-filter-checkbox:checked").each(function () {
191        if ($(this).hasClass("js-news-filter-all")) return;
192        labels.push($(this).data("label"));
193      });
194      if (!labels.length) {
195        $value.text(defaultLabel);
196        return;
197      }
198      if (labels.length === 1) {
199        $value.text(labels[0]);
200        return;
201      }
202      var selectedText = i18n.selected || "%d selected";
203      $value.text(selectedText.replace("%d", String(labels.length)));
204    });
205  }
206
207  function ensureFilterResultsBar($root) {
208    var $existing = $root.find(".js-news-filter-results");
209    if ($existing.length) {
210      return $existing;
211    }
212
213    var $bar = $(
214      '<div class="filter-results pol js-news-filter-results" role="region"></div>'
215    );
216    $bar.attr("aria-label", i18n.activeFilters || "Active news filters");
217    $bar.append(
218      '<div class="filter-results__bar" aria-label="' +
219        escapeHtml(i18n.activeFiltersBar || "Active filters") +
220        '">' +
221        '<ul class="filter-results__tags js-news-active-tags" aria-label="' +
222        escapeHtml(i18n.removeFiltersList || "Remove individual filters") +
223        '"></ul>' +
224        '<button class="l-btn-simple l-btn-simple--light filter-results__clear-all js-news-clear-all" type="button" title="' +
225        escapeHtml(i18n.clearFiltersTitle || "Clear all active filters") +
226        '">' +
227        escapeHtml(i18n.clearFilters || "Clear filters") +
228        "</button></div>"
229    );
230    $root.find(".latest-news__header").after($bar);
231    return $bar;
232  }
233
234  function renderActiveTags($root) {
235    var tags = [];
236
237    $root.find(".js-news-filter-checkbox:checked").each(function () {
238      var $input = $(this);
239      if ($input.hasClass("js-news-filter-all")) return;
240      tags.push({
241        filter: $input.closest(".js-news-filter").data("filter"),
242        year: String($input.val()),
243        label: $input.data("label"),
244      });
245    });
246
247    if (!tags.length) {
248      $root.find(".js-news-filter-results").remove();
249      return;
250    }
251
252    var $bar = ensureFilterResultsBar($root);
253    var $list = $bar.find(".js-news-active-tags");
254    $list.empty();
255
256    tags.forEach(function (tag) {
257      var removeLabel = (i18n.removeFilter || "Remove filter: %s").replace(
258        "%s",
259        tag.label
260      );
261      var $item = $("<li></li>");
262      var $btn = $(
263        '<button class="filter-results__tag" type="button"></button>'
264      );
265      $btn.attr("aria-label", removeLabel);
266      $btn.attr("data-filter", tag.filter);
267      $btn.attr("data-year", tag.year);
268      $btn.append(
269        '<figure aria-hidden="true"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" focusable="false"><line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line><line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></line></svg></figure>'
270      );
271      $btn.append($('<span class="p--sm"></span>').text(tag.label));
272      $item.append($btn);
273      $list.append($item);
274    });
275  }
276
277  function escapeHtml(value) {
278    return String(value || "")
279      .replace(/&/g, "&amp;")
280      .replace(/</g, "&lt;")
281      .replace(/>/g, "&gt;")
282      .replace(/"/g, "&quot;");
283  }
284
285  function renderCardHtml(card) {
286    var permalink = card.permalink || "";
287    var linkTitle = card.link_title || card.title || "";
288    var image = card.image || {};
289    var imageHtml = image.url
290      ? '<figure class="news-card__media"><picture><img src="' +
291        escapeHtml(image.url) +
292        '" alt="' +
293        escapeHtml(image.alt) +
294        '" width="' +
295        escapeHtml(image.width || 443) +
296        '" height="' +
297        escapeHtml(image.height || 303) +
298        '" loading="lazy" decoding="async" /></picture></figure>'
299      : "";
300    var categoryHtml = card.category
301      ? '<p class="label--med news-card__category">' +
302        escapeHtml(card.category) +
303        "</p>"
304      : "";
305    var excerptHtml = card.excerpt
306      ? '<p class="p--sm news-card__excerpt">' +
307        escapeHtml(card.excerpt) +
308        "</p>"
309      : "";
310    var openLink = permalink
311      ? '<a class="news-card__link" href="' +
312        escapeHtml(permalink) +
313        '" title="' +
314        escapeHtml(linkTitle) +
315        '">'
316      : "";
317    var closeLink = permalink ? "</a>" : "";
318
319    return (
320      '<li class="news-card-listing__item"><article class="pol news-card">' +
321      openLink +
322      imageHtml +
323      '<div class="news-card__body"><div class="news-card__intro">' +
324      categoryHtml +
325      '<h3 class="news-card__title">' +
326      escapeHtml(card.title) +
327      "</h3></div>" +
328      excerptHtml +
329      "</div>" +
330      closeLink +
331      "</article></li>"
332    );
333  }
334
335  function renderGrid($root, cards) {
336    var $grid = $root.find(".js-news-grid");
337    $grid.empty();
338
339    if (!cards || !cards.length) {
340      $grid.append(
341        '<li class="news-card-listing__item"><p>' +
342          escapeHtml(
343            i18n.noResults || "No news matches the selected filters."
344          ) +
345          "</p></li>"
346      );
347      return;
348    }
349
350    cards.forEach(function (card) {
351      $grid.append(renderCardHtml(card));
352    });
353  }
354
355  function getMaxWindowStart(maxPages) {
356    return Math.max(1, maxPages - windowSize + 1);
357  }
358
359  function alignWindowStart(page, maxPages, requestedStart) {
360    if (maxPages < 2) return 1;
361
362    page = Math.max(1, Math.min(page, maxPages));
363    var maxStart = getMaxWindowStart(maxPages);
364    var start = Math.max(1, Math.min(requestedStart || 1, maxStart));
365
366    if (page < start) {
367      start = page;
368    } else if (page > start + windowSize - 1) {
369      start = page - windowSize + 1;
370    }
371
372    return Math.max(1, Math.min(start, maxStart));
373  }
374
375  function getWindowPages(windowStart, maxPages) {
376    if (maxPages < 2) return [];
377
378    if (maxPages <= windowSize) {
379      var all = [];
380      for (var n = 1; n <= maxPages; n++) all.push(n);
381      return all;
382    }
383
384    var start = alignWindowStart(windowStart, maxPages, windowStart);
385    var end = Math.min(maxPages, start + windowSize - 1);
386    var pages = [];
387    for (var i = start; i <= end; i++) pages.push(i);
388    return pages;
389  }
390
391  function getLoaderShell($root) {
392    return $root.find(".js-news-loader");
393  }
394
395  function getLoaderContent($root) {
396    return $root.find(".js-news-loader-content");
397  }
398
399  function renderPagination($root, page, maxPages, windowStart) {
400    var $existing = $root.find(".js-news-pagination");
401    $existing.remove();
402
403    if (maxPages < 2) return;
404
405    windowStart = alignWindowStart(
406      page,
407      maxPages,
408      typeof windowStart === "number"
409        ? windowStart
410        : $root.data("news-window-start") || 1
411    );
412    var windowPages = getWindowPages(windowStart, maxPages);
413    if (!windowPages.length) return;
414
415    $root.data("news-window-start", windowStart);
416    $root.data("news-max-pages", maxPages);
417
418    var showWindowPrev = page > 1;
419    var showWindowNext = page < maxPages;
420
421    var $nav = $(
422      '<nav class="pol pagination js-news-pagination" aria-label="News pages"></nav>'
423    );
424    $nav.attr("data-window-start", String(windowStart));
425    $nav.attr("data-max-pages", String(maxPages));
426    var $list = $('<ul class="pagination__list"></ul>');
427    var arrowSvg =
428      '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" viewBox="0 0 10 12" fill="none" aria-hidden="true" focusable="false"><path d="M9.75 5.62891L1.44639e-07 11.2581L1.03846e-08 -0.000258806L9.75 5.62891Z" fill="#AAE3DD"></path></svg>';
429
430    if (showWindowPrev) {
431      var $windowPrev = $(
432        '<button class="pagination__prev js-news-window-prev" type="button" aria-label="Previous page"></button>'
433      );
434      $windowPrev.append(arrowSvg);
435      $list.append($("<li></li>").append($windowPrev));
436    }
437
438    windowPages.forEach(function (pageNumber) {
439      var $pageBtn = $(
440        '<button class="pagination__btn js-news-page-btn" type="button"></button>'
441      );
442      $pageBtn.attr("data-page", String(pageNumber));
443      $pageBtn.attr("aria-label", "Page " + pageNumber);
444      $pageBtn.text(String(pageNumber));
445      if (pageNumber === page) {
446        $pageBtn.addClass("is-active").attr("aria-current", "page");
447      }
448      $list.append($("<li></li>").append($pageBtn));
449    });
450
451    if (showWindowNext) {
452      var $windowNext = $(
453        '<button class="pagination__next js-news-window-next" type="button" aria-label="Next page"></button>'
454      );
455      $windowNext.append(arrowSvg);
456      $list.append($("<li></li>").append($windowNext));
457    }
458
459    $nav.append($list);
460    getLoaderContent($root).append($nav);
461  }
462
463  function scrollToListing($root) {
464    if (typeof win.polScrollToListing === "function") {
465      win.polScrollToListing($root, $root.find(".js-news-grid"));
466    }
467  }
468
469  function fetchNews($root, page) {
470    if (!ajaxUrl) return;
471
472    var filters = getSelectedFilters($root);
473    var topPostIds = getTopPostIds($root);
474    var $loader = getLoaderShell($root);
475    $loader.addClass("is-loading");
476
477    fetch(ajaxUrl, {
478      method: "POST",
479      credentials: "same-origin",
480      body: buildAjaxBody(filters, page, topPostIds),
481    })
482      .then(function (response) {
483        if (!response.ok) throw new Error("News request failed");
484        return response.json();
485      })
486      .then(function (data) {
487        renderGrid($root, data.cards || []);
488        renderPagination($root, data.page || 1, data.max_pages || 0);
489        $root.data("news-page", data.page || 1);
490      })
491      .catch(function () {
492        renderGrid($root, []);
493        renderPagination($root, 1, 0);
494      })
495      .finally(function () {
496        $loader.removeClass("is-loading");
497      });
498  }
499
500  function applyFilters($root, page, shouldScroll, urlOptions) {
501    urlOptions = urlOptions || {};
502    var currentPage = page || 1;
503    var filters = getSelectedFilters($root);
504    if (!shouldScroll) {
505      $root.data("news-window-start", 1);
506    }
507    updateFilterLabels($root);
508    renderActiveTags($root);
509    $root.data("news-page", currentPage);
510    if (!urlOptions.skipUrlUpdate) {
511      updateBrowserUrl(filters, currentPage, !!urlOptions.replaceUrl);
512    }
513    fetchNews($root, currentPage);
514    if (shouldScroll) {
515      scrollToListing($root);
516    }
517  }
518
519  function clearAllFilters($root) {
520    $root.find(".js-news-filter-checkbox").prop("checked", false);
521    $root.find(".js-news-filter .dropdown__option").attr("aria-selected", "false");
522    applyFilters($root, 1);
523  }
524
525  function initNews($root) {
526    if (!$root.length) return;
527
528    var $pagination = $root.find(".js-news-pagination");
529    if ($pagination.length) {
530      $root.data(
531        "news-window-start",
532        parseInt($pagination.attr("data-window-start"), 10) || 1
533      );
534      $root.data(
535        "news-max-pages",
536        parseInt($pagination.attr("data-max-pages"), 10) || 0
537      );
538    }
539
540    var instanceId = ++instanceCount;
541    var filterNs = ".newsFilter-" + instanceId;
542    var filters = getFiltersFromUrl();
543    var page = getPageFromUrl();
544
545    setFiltersFromState($root, filters);
546    $root.data("news-page", page);
547    updateFilterLabels($root);
548    renderActiveTags($root);
549
550    $(win).on("popstate" + filterNs, function () {
551      syncStateFromUrl($root, true, false);
552    });
553
554    $root.on("click.news", ".js-news-filter-btn", function () {
555      var $filter = $(this).closest(".js-news-filter");
556      var isOpen = $(this).attr("aria-expanded") === "true";
557      if (isOpen) {
558        closeFilter($filter);
559        return;
560      }
561      $root.find(".js-news-filter").not($filter).each(function () {
562        closeFilter($(this));
563      });
564      openFilter($filter);
565    });
566
567    $(doc).on("click" + filterNs, function (evt) {
568      if ($(evt.target).closest(".js-news-filter", $root[0]).length) {
569        return;
570      }
571      $root.find(".js-news-filter").each(function () {
572        closeFilter($(this));
573      });
574    });
575
576    $root.on("change.news", ".js-news-filter-checkbox", function () {
577      var $input = $(this);
578      var $filter = $input.closest(".js-news-filter");
579
580      if ($input.hasClass("js-news-filter-all")) {
581        if ($input.is(":checked")) {
582          $filter
583            .find(".js-news-filter-checkbox")
584            .not(".js-news-filter-all")
585            .prop("checked", false)
586            .each(function () {
587              $(this).closest(".dropdown__option").attr("aria-selected", "false");
588            });
589        }
590      } else if ($input.is(":checked")) {
591        $filter.find(".js-news-filter-all").prop("checked", false);
592        $filter
593          .find(".js-news-filter-all")
594          .closest(".dropdown__option")
595          .attr("aria-selected", "false");
596      }
597
598      var $option = $input.closest(".dropdown__option");
599      $option.attr(
600        "aria-selected",
601        $input.is(":checked") ? "true" : "false"
602      );
603      applyFilters($root, 1);
604    });
605
606    $root.on(
607      "click.news",
608      ".js-news-active-tags .filter-results__tag",
609      function () {
610        var filterKey = $(this).data("filter");
611        var year = String($(this).data("year"));
612        $root
613          .find(
614            '.js-news-filter[data-filter="' +
615              filterKey +
616              '"] .js-news-filter-checkbox'
617          )
618          .filter(function () {
619            return String($(this).val()) === year;
620          })
621          .prop("checked", false)
622          .trigger("change");
623      }
624    );
625
626    $root.on("click.news", ".js-news-clear-all", function () {
627      clearAllFilters($root);
628    });
629
630    $root.on("click.news", ".js-news-page-btn", function () {
631      var page = parseInt($(this).data("page"), 10);
632      if (!page) return;
633      applyFilters($root, page, true);
634    });
635
636    $root.on("click.news", ".js-news-window-prev", function () {
637      var maxPages = parseInt($root.data("news-max-pages"), 10) || 0;
638      var page = parseInt($root.data("news-page"), 10) || 1;
639      if (page <= 1) return;
640
641      var newPage = page - 1;
642      var windowStart = parseInt($root.data("news-window-start"), 10) || 1;
643      windowStart = alignWindowStart(
644        newPage,
645        maxPages,
646        Math.max(1, windowStart - windowStep)
647      );
648      $root.data("news-window-start", windowStart);
649      applyFilters($root, newPage, true);
650    });
651
652    $root.on("click.news", ".js-news-window-next", function () {
653      var maxPages = parseInt($root.data("news-max-pages"), 10) || 0;
654      var page = parseInt($root.data("news-page"), 10) || 1;
655      if (page >= maxPages) return;
656
657      var newPage = page + 1;
658      var windowStart = parseInt($root.data("news-window-start"), 10) || 1;
659      windowStart = alignWindowStart(
660        newPage,
661        maxPages,
662        Math.min(getMaxWindowStart(maxPages), windowStart + windowStep)
663      );
664      $root.data("news-window-start", windowStart);
665      applyFilters($root, newPage, true);
666    });
667  }
668
669  $(function () {
670    $(".js-news-listing").each(function () {
671      initNews($(this));
672    });
673  });
674})(window.jQuery, window, document);

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.