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, "&") 280 .replace(/</g, "<") 281 .replace(/>/g, ">") 282 .replace(/"/g, """); 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.