1(function($) { 2 'use strict'; 3 4 // --- State --- 5 var state = { 6 daily_tag: '', 7 email_source: [], 8 newsletter_category: '', 9 search: '', 10 page: 1 11 }; 12 13 var $wrapper, $feedList, $skeletonTpl, $overlay, searchTimer; 14 var isLoading = false; 15 16 // --- Init --- 17 $(document).ready(function() { 18 $wrapper = $('.tznl-wrapper'); 19 if (!$wrapper.length) return; 20 21 $feedList = $wrapper.find('.tznl-feed-list'); 22 $skeletonTpl = $wrapper.find('.tznl-skeleton-template'); 23 $overlay = $wrapper.find('.tznl-modal-overlay'); 24 25 bindEvents(); 26 initInfiniteScroll(); 27 }); 28 29 function bindEvents() { 30 // --- Dropdown toggles --- 31 $wrapper.on('click', '.tznl-filter-icon-btn', function(e) { 32 e.stopPropagation(); 33 var type = $(this).data('dropdown'); 34 var $dropdown = $wrapper.find('.tznl-dropdown-' + type); 35 var isVisible = $dropdown.is(':visible'); 36 37 closeDropdowns(); 38 39 if (!isVisible) { 40 // On mobile, add backdrop 41 if (window.innerWidth <= 768) { 42 $('<div class="tznl-dropdown-backdrop"></div>').insertBefore($dropdown); 43 } 44 $dropdown.show(); 45 $(this).addClass('active'); 46 } 47 }); 48 49 $wrapper.on('click', '.tznl-dropdown-close', function() { 50 closeDropdowns(); 51 }); 52 53 $(document).on('click', '.tznl-dropdown-backdrop', function() { 54 closeDropdowns(); 55 }); 56 57 $(document).on('click', function(e) { 58 if (!$(e.target).closest('.tznl-filter-toggles').length && !$(e.target).closest('.tznl-dropdown').length) { 59 closeDropdowns(); 60 } 61 }); 62 63 // --- Date filter --- 64 $wrapper.on('click', '.tznl-date-btn', function() { 65 var slug = $(this).data('slug'); 66 var wasActive = $(this).hasClass('active'); 67 68 $wrapper.find('.tznl-date-btn').removeClass('active'); 69 70 if (wasActive) { 71 state.daily_tag = ''; 72 } else { 73 $(this).addClass('active'); 74 state.daily_tag = slug; 75 } 76 resetAndLoad(); 77 }); 78 79 // --- Source filter: row click (multi-select toggle) --- 80 $wrapper.on('click', '.tznl-source-item', function(e) { 81 if ($(e.target).closest('.tznl-source-only-btn').length) return; // let Only handle itself 82 var slug = $(this).find('.tznl-source-only-btn').data('slug'); 83 var idx = state.email_source.indexOf(slug); 84 85 if (idx > -1) { 86 state.email_source.splice(idx, 1); 87 $(this).removeClass('active'); 88 } else { 89 state.email_source.push(slug); 90 $(this).addClass('active'); 91 } 92 resetAndLoad(); 93 }); 94 95 // --- Source filter: Only button (single-select) --- 96 $wrapper.on('click', '.tznl-source-only-btn', function(e) { 97 e.stopPropagation(); 98 var slug = $(this).data('slug'); 99 var isOnlyActive = state.email_source.length === 1 && state.email_source[0] === slug; 100 101 $wrapper.find('.tznl-source-item').removeClass('active'); 102 state.email_source = []; 103 104 if (!isOnlyActive) { 105 state.email_source = [slug]; 106 $(this).closest('.tznl-source-item').addClass('active'); 107 } 108 resetAndLoad(); 109 }); 110 111 // --- Category tabs --- 112 $wrapper.on('click', '.tznl-cat-tab', function() { 113 $wrapper.find('.tznl-cat-tab').removeClass('tznl-cat-active'); 114 $(this).addClass('tznl-cat-active'); 115 state.newsletter_category = $(this).data('slug'); 116 resetAndLoad(); 117 }); 118 119 // --- Search (debounced) --- 120 $wrapper.on('input', '.tznl-search-input', function() { 121 var val = $(this).val(); 122 clearTimeout(searchTimer); 123 searchTimer = setTimeout(function() { 124 state.search = val; 125 resetAndLoad(); 126 }, 400); 127 }); 128 129 // Infinite scroll: observer is initialized via initInfiniteScroll(). 130 // No click handler needed â the load-more button has been replaced 131 // by a viewport-triggered sentinel. 132 133 // --- Read all / Show less toggle --- 134 $wrapper.on('click', '.tznl-read-toggle', function(e) { 135 e.preventDefault(); 136 var $container = $(this).closest('.tznl-excerpt-container'); 137 $container.find('.tznl-excerpt-short, .tznl-excerpt-full').toggle(); 138 }); 139 140 // --- Source Newsletter modal --- 141 $wrapper.on('click', '.tznl-source-btn', function() { 142 var postId = $(this).data('post-id'); 143 openModal(postId); 144 }); 145 146 // --- Close modal --- 147 $overlay.on('click', '.tznl-modal-close', function() { 148 closeModal(); 149 }); 150 $overlay.on('click', function(e) { 151 if ($(e.target).is('.tznl-modal-overlay')) { 152 closeModal(); 153 } 154 }); 155 $(document).on('keydown', function(e) { 156 if (e.key === 'Escape' && $overlay.is(':visible')) { 157 closeModal(); 158 } 159 }); 160 161 // --- Clear filter tags --- 162 $wrapper.on('click', '.tznl-clear-filter', function() { 163 var type = $(this).data('type'); 164 if (type === 'daily_tag') { 165 state.daily_tag = ''; 166 $wrapper.find('.tznl-date-btn').removeClass('active'); 167 } else if (type === 'email_source') { 168 var slug = $(this).data('slug'); 169 if (slug) { 170 var idx = state.email_source.indexOf(slug); 171 if (idx > -1) state.email_source.splice(idx, 1); 172 $wrapper.find('.tznl-source-only-btn[data-slug="' + slug + '"]').closest('.tznl-source-item').removeClass('active'); 173 } else { 174 state.email_source = []; 175 $wrapper.find('.tznl-source-item').removeClass('active'); 176 } 177 } 178 resetAndLoad(); 179 }); 180 } 181 182 // --- Close all dropdowns and remove backdrop --- 183 function closeDropdowns() { 184 $wrapper.find('.tznl-dropdown').hide(); 185 $wrapper.find('.tznl-filter-icon-btn').removeClass('active'); 186 $('.tznl-dropdown-backdrop').remove(); 187 } 188 189 // --- Infinite Scroll --- 190 // IntersectionObserver watches a sentinel element at the end of the 191 // feed. When it enters the viewport we fetch the next page. The 192 // sentinel is re-rendered by each AJAX response so we re-observe 193 // after every append; `infiniteDone` short-circuits once the server 194 // reports no more pages. 195 var io = null; 196 var infiniteDone = false;
197 198 function initInfiniteScroll() { 199 if (typeof IntersectionObserver === 'undefined') return; 200 io = new IntersectionObserver(function(entries) { 201 entries.forEach(function(entry) { 202 if (!entry.isIntersecting) return; 203 if (isLoading || infiniteDone) return; 204 var next = parseInt($(entry.target).data('next-page'), 10); 205 if (!next) return; 206 // Reveal the "Loading more newsletters..." indicator while 207 // the fetch is in flight. 208 $(entry.target).addClass('is-loading'); 209 state.page = next; 210 loadMore(); 211 }); 212 }, { rootMargin: '400px 0px' }); 213 observeSentinel(); 214 } 215 216 function observeSentinel() { 217 if (!io) return; 218 var sentinel = $wrapper.find('.tznl-infinite-sentinel')[0]; 219 if (sentinel) io.observe(sentinel); 220 } 221 222 function sentinelMarkup(nextPage) { 223 return '<div class="tznl-infinite-sentinel" data-next-page="' + nextPage + '" aria-hidden="true">' 224 + '<div class="tznl-infinite-loader"><span class="tznl-btn-spinner"></span> Loading more newsletters...</div>' 225 + '</div>'; 226 } 227 228 // --- Reset filters and load fresh (replaces content) --- 229 function resetAndLoad() { 230 state.page = 1; 231 infiniteDone = false; 232 showSkeleton(); 233 updateActiveFilters(); 234 doAjaxLoad(false); 235 } 236 237 // --- Load More (appends content) --- 238 function loadMore() { 239 isLoading = true; 240 doAjaxLoad(true); 241 } 242 243 // --- Core AJAX loader --- 244 function doAjaxLoad(isAppend) { 245 $.ajax({ 246 url: tznl_ajax.ajax_url, 247 type: 'POST', 248 data: { 249 action: 'tznl_load_posts', 250 nonce: tznl_ajax.nonce, 251 daily_tag: state.daily_tag, 252 email_source: state.email_source.join(','), 253 newsletter_category: state.newsletter_category, 254 search: state.search, 255 page: state.page, 256 posts_per_page: $wrapper.data('posts-per-page') || 15, 257 is_load_more: isAppend ? '1' : '0' 258 }, 259 success: function(res) { 260 isLoading = false; 261 262 if (!res.success) { 263 if (!isAppend) { 264 $feedList.html('<div class="tznl-no-results"><p>No newsletters found.</p></div>'); 265 } 266 $wrapper.find('.tznl-infinite-sentinel').remove(); 267 infiniteDone = true; 268 return; 269 } 270 271 var d = res.data; 272 273 if (isAppend) { 274 // Append: merge date groups intelligently 275 var $newContent = $('<div>').html(d.html); 276 var $lastGroup = $feedList.find('.tznl-date-group:last'); 277 var lastHeading = $lastGroup.find('.tznl-date-heading').text(); 278 279 $newContent.find('.tznl-date-group').each(function() { 280 var $group = $(this); 281 var heading = $group.find('.tznl-date-heading').text(); 282 283 if (heading === lastHeading && $lastGroup.length) { 284 // Same date group â append cards without duplicate heading 285 $group.find('.tznl-card').each(function() { 286 $lastGroup.append($(this)); 287 }); 288 } else { 289 // New date group â append entire group 290 $feedList.append($group); 291 $lastGroup = $group; 292 lastHeading = heading; 293 } 294 }); 295 } else { 296 // Replace 297 $feedList.html(d.html); 298 } 299 300 // Swap the sentinel for infinite scroll 301 $wrapper.find('.tznl-infinite-sentinel').remove(); 302 if (d.has_more) { 303 infiniteDone = false;
304 $feedList.after(sentinelMarkup(d.next_page)); 305 observeSentinel(); 306 } else { 307 infiniteDone = true; 308 } 309 }, 310 error: function() { 311 isLoading = false; 312 if (!isAppend) { 313 $feedList.html('<div class="tznl-no-results"><p>Failed to load. Please try again.</p></div>'); 314 } 315 $wrapper.find('.tznl-infinite-sentinel').remove(); 316 infiniteDone = true; 317 } 318 }); 319 } 320 321 // --- Skeleton --- 322 function showSkeleton() { 323 $feedList.html($skeletonTpl.html()); 324 // A new filter/search starts over â drop any stale sentinel; the 325 // response will re-render a fresh one if there are more pages. 326 $wrapper.find('.tznl-infinite-sentinel').remove(); 327 infiniteDone = false; 328 } 329 330 // --- Active Filter Tags --- 331 function updateActiveFilters() { 332 $wrapper.find('.tznl-active-filters').remove(); 333 334 var tags = []; 335 if (state.daily_tag) { 336 var label = $wrapper.find('.tznl-date-btn[data-slug="' + state.daily_tag + '"]').text() || state.daily_tag; 337 tags.push('<span class="tznl-active-filter-tag">' + label + ' <button type="button" class="tznl-clear-filter" data-type="daily_tag">×</button></span>'); 338 } 339 if (state.email_source.length) { 340 state.email_source.forEach(function(slug) { 341 tags.push('<span class="tznl-active-filter-tag">' + slug + ' <button type="button" class="tznl-clear-filter" data-type="email_source" data-slug="' + slug + '">×</button></span>'); 342 }); 343 } 344 345 if (tags.length) { 346 $feedList.before('<div class="tznl-active-filters">' + tags.join('') + '</div>'); 347 } 348 } 349 350 // --- Modal --- 351 function openModal(postId) { 352 $overlay.find('.tznl-modal-header').hide(); 353 $overlay.find('.tznl-modal-body').html('<div class="tznl-modal-loading"><div class="tznl-modal-spinner"></div></div>'); 354 $overlay.css('display', 'flex'); 355 356 $overlay[0].offsetHeight; 357 $overlay.addClass('tznl-modal-visible'); 358 document.body.style.overflow = 'hidden'; 359 360 $.ajax({ 361 url: tznl_ajax.ajax_url, 362 type: 'POST', 363 data: { 364 action: 'tznl_load_modal', 365 nonce: tznl_ajax.nonce, 366 post_id: postId 367 }, 368 success: function(res) { 369 if (res.success) { 370 var d = res.data; 371 $overlay.find('.tznl-modal-logo').attr('src', d.source_logo); 372 $overlay.find('.tznl-modal-source-name').text(d.source_name); 373 $overlay.find('.tznl-modal-title').text(d.title); 374 $overlay.find('.tznl-modal-date').text(d.date + ' | ' + d.time); 375 $overlay.find('.tznl-modal-header').show(); 376 $overlay.find('.tznl-modal-body').html(d.content); 377 } else { 378 $overlay.find('.tznl-modal-body').html('<p>Failed to load content.</p>'); 379 } 380 }, 381 error: function() { 382 $overlay.find('.tznl-modal-body').html('<p>Network error. Please try again.</p>'); 383 } 384 }); 385 } 386 387 function closeModal() { 388 $overlay.removeClass('tznl-modal-visible'); 389 document.body.style.overflow = ''; 390 setTimeout(function() { 391 $overlay.hide(); 392 $overlay.find('.tznl-modal-body').html(''); 393 }, 250); 394 } 395 396})(jQuery);
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.