1(function ($) { 2 var $page = $('.services-page').first(); 3 var config = window.HCTServicesPage || {}; 4 var labels = config.labels || {}; 5 var messages = config.messages || {}; 6 var khadamatiNames = config.khadamatiNames || {}; 7 var $search; 8 var $category; 9 var $trigger; 10 var $listing; 11 var $status; 12 var $paginationWrap; 13 var $desktopPages; 14 var $mobilePages; 15 var $first; 16 var $prev; 17 var $next; 18 var $last; 19 var state; 20 21 if (!$page.length) { 22 return; 23 } 24 25 $search = $('#service-keyword'); 26 $category = $('#service-category'); 27 $trigger = $('.js-services-search-trigger'); 28 $listing = $('#services-listing'); 29 $status = $('#services-status'); 30 $paginationWrap = $('#services-pagination-wrap'); 31 $desktopPages = $('#services-pagination-pages'); 32 $mobilePages = $('#services-mobile-pages'); 33 $first = $('.js-services-pagination-first'); 34 $prev = $('.js-services-pagination-prev'); 35 $next = $('.js-services-pagination-next'); 36 $last = $('.js-services-pagination-last'); 37 38 if (!$search.length || !$category.length || !$trigger.length || !$listing.length) { 39 return; 40 } 41 42 state = { 43 search: '', 44 category: $category.val() || '', 45 page: 1, 46 perPage: 16, 47 totalPages: 1, 48 totalItems: 0, 49 loading: false 50 }; 51 52 function safeText(value) { 53 return $('<div/>').text(value == null ? '' : String(value)).html(); 54 } 55 56 function decodeEntities(value) { 57 var element = document.createElement('textarea'); 58 59 element.innerHTML = String(value == null ? '' : value); 60 61 return element.value; 62 } 63 64 function normalizeUrl(value) { 65 return String(value == null ? '' : value).trim(); 66 } 67 68 function getDisplayTitle(item) { 69 var serviceId = item && item.acf && item.acf.khadamati_service_id ? String(item.acf.khadamati_service_id) : ''; 70 var wpTitle = item && item.title && item.title.rendered ? decodeEntities(item.title.rendered) : ''; 71 var khadamatiTitle = serviceId && khadamatiNames[serviceId] ? decodeEntities(khadamatiNames[serviceId]) : ''; 72 73 return khadamatiTitle || wpTitle; 74 } 75 76 function getDescription(item) { 77 var description = item && item.acf && item.acf.service_description ? item.acf.service_description : ''; 78 79 return decodeEntities(description); 80 } 81 82 function getBookmarks() { 83 var raw; 84 85 try { 86 raw = window.localStorage.getItem('hct-services-bookmarks'); 87 return raw ? JSON.parse(raw) : {}; 88 } catch (error) { 89 return {}; 90 } 91 } 92 93 function setBookmarks(bookmarks) { 94 try { 95 window.localStorage.setItem('hct-services-bookmarks', JSON.stringify(bookmarks)); 96 } catch (error) { 97 return; 98 } 99 } 100 101 function getBookmarkKey(item) { 102 if (item && item.id != null) { 103 return String(item.id); 104 } 105 106 return normalizeUrl(item && item.link ? item.link : ''); 107 } 108 109 function setStatus(message, visible) { 110 if (!message || visible === false) { 111 $status.addClass('hidden').text(''); 112 return; 113 } 114 115 $status.removeClass('hidden').text(message); 116 } 117 118 function setLoading(isLoading) { 119 state.loading = isLoading; 120 121 if (isLoading) { 122 setStatus(messages.loading || 'Loading services...', true); 123 $paginationWrap.addClass('opacity-50'); 124 return; 125 } 126 127 if ($status.text() === (messages.loading || 'Loading services...')) { 128 setStatus('', false); 129 } 130 131 $paginationWrap.removeClass('opacity-50'); 132 } 133 134 function renderMessage(message) { 135 $listing.html('<div class="col-span-full py-8 text-center">' + safeText(message) + '</div>'); 136 } 137 138 function renderItems(items) { 139 var content = ''; 140 141 $.each(items, function (index, item) { 142 var title = getDisplayTitle(item); 143 var description = getDescription(item); 144 var detailUrl = normalizeUrl(item && item.link ? item.link : '#'); 145 var startUrl = normalizeUrl(item && item.acf && item.acf.service_start ? item.acf.service_start : ''); 146 var startMarkup = ''; 147 var bookmarkKey = getBookmarkKey(item); 148 var isSaved = !!getBookmarks()[bookmarkKey]; 149 var bookmarkTitle = isSaved ? (labels.saved || 'Service saved') : (labels.save || 'Save service'); 150 151 if (startUrl !== '') { 152 startMarkup = '<a href="' + safeText(startUrl) + '" class="aegov-link" target="_blank" rel="noopener">' + 153 safeText(labels.start_service || 'Start service') + 154 '<svg class="link-icon rtl:-scale-x-100" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect>
154<line x1="40" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><polyline points="144 56 216 128 144 200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></polyline></svg>' + 155 '</a>'; 156 } 157 158 content += '<div class="aegov-card card-bordered card-glow card-service h-full flex flex-col justify-between gap-4">'; 159 content += '<div class="flex flex-col gap-3">'; 160 content += '<h3 class="card-service-title mb-0"><a href="' + safeText(detailUrl) + '" class="line-clamp-2">' + safeText(title) + '</a></h3>'; 161 162 if (description !== '') { 163 content += '<p class="line-clamp-3 mb-0">' + safeText(description) + '</p>'; 164 } 165 166 content += '</div>'; 167 content += '<div class="flex items-center justify-between gap-4 flex-wrap">'; 168 content += '<div class="flex items-center flex-wrap gap-4">'; 169 content += startMarkup; 170 content += '</div>'; 171 content += '<button type="button" class="text-primary-600 hover:text-primary-500 group js-service-bookmark' + (isSaved ? ' checked' : '') + '" data-bookmark-key="' + safeText(bookmarkKey) + '" aria-pressed="' + (isSaved ? 'true' : 'false') + '" aria-label="' + safeText(bookmarkTitle) + '" title="' + safeText(bookmarkTitle) + '">'; 172 content += '<svg class="group-[.checked]:hidden" xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M184,32H72A16,16,0,0,0,56,48V224a8,8,0,0,0,12.24,6.78L128,193.43l59.77,37.35A8,8,0,0,0,200,224V48A16,16,0,0,0,184,32Zm0,177.57-51.77-32.35a8,8,0,0,0-8.48,0L72,209.57V48H184Z"></path></svg>'; 173 content += '<svg class="hidden group-[.checked]:block fill-primary-600" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path d="M184,32H72A16,16,0,0,0,56,48V224a8,8,0,0,0,12.24,6.78L128,193.43l59.77,37.35A8,8,0,0,0,200,224V48A16,16,0,0,0,184,32Z"></path></svg>'; 174 content += '</button>'; 175 content += '</div>'; 176 content += '</div>'; 177 }); 178 179 $listing.html(content); 180 } 181 182 function buildPageList(currentPage, totalPages) { 183 var pages = []; 184 var start; 185 var end; 186 var page; 187 188 if (totalPages <= 7) { 189 for (page = 1; page <= totalPages; page += 1) { 190 pages.push(page); 191 } 192 193 return pages; 194 } 195 196 pages.push(1); 197 start = Math.max(2, currentPage - 1); 198 end = Math.min(totalPages - 1, currentPage + 1); 199 200 if (currentPage <= 3) { 201 end = 4; 202 } 203 204 if (currentPage >= totalPages - 2) { 205 start = totalPages - 3; 206 } 207 208 if (start > 2) { 209 pages.push('ellipsis-start'); 210 } 211 212 for (page = start; page <= end; page += 1) { 213 pages.push(page); 214 } 215 216 if (end < totalPages - 1) { 217 pages.push('ellipsis-end'); 218 } 219 220 pages.push(totalPages); 221 222 return pages; 223 } 224 225 function setControlState($elements, disabled) { 226 $elements 227 .toggleClass('pointer-events-none opacity-50', !!disabled) 228 .attr('aria-disabled', disabled ? 'true' : 'false') 229 .attr('tabindex', disabled ? '-1' : '0'); 230 } 231 232 function renderPagination() { 233 var pages; 234 var desktopMarkup = ''; 235 236 if (state.totalItems < 1 || state.totalPages <= 1) { 237 $paginationWrap.addClass('hidden').removeClass('flex'); 238 return; 239 } 240 241 pages = buildPageList(state.page, state.totalPages); 242 243 $.each(pages, function (index, item) { 244 if (typeof item === 'string') { 245 desktopMarkup += '<span class="aegov-pagination-extend">...</span>'; 246 return; 247 } 248 249 if (item === state.page) { 250 desktopMarkup += '<span aria-current="page" class="aegov-pagination-current">' + safeText(item) + '</span>'; 251 return; 252 } 253 254 desktopMarkup += '<a class="aegov-pagination-page aegov-pagination-larger js-services-pagination-page" title="' + s
254afeText((labels.page || 'Page') + ' ' + item) + '" data-page="' + item + '" href="#">' + safeText(item) + '</a>'; 255 }); 256 257 $desktopPages.html(desktopMarkup); 258 $mobilePages.text((labels.page || 'Page') + ' ' + state.page + ' ' + (labels.of || 'of') + ' ' + state.totalPages); 259 $paginationWrap.removeClass('hidden').addClass('flex'); 260 261 setControlState($first, state.page === 1); 262 setControlState($prev, state.page === 1); 263 setControlState($next, state.page === state.totalPages); 264 setControlState($last, state.page === state.totalPages); 265 } 266 267 function renderResults(items) { 268 if (!Array.isArray(items) || !items.length) { 269 renderMessage(messages.noResults || 'No results found.'); 270 $paginationWrap.addClass('hidden').removeClass('flex'); 271 return; 272 } 273 274 setStatus('', false); 275 renderItems(items); 276 renderPagination(); 277 } 278 279 function fetchServices() { 280 var params = new URLSearchParams(); 281 282 if (state.loading) { 283 return; 284 } 285 286 setLoading(true); 287 288 params.set('per_page', String(state.perPage)); 289 params.set('page', String(state.page)); 290 params.set('lang', config.lang || 'en'); 291 params.append('_fields[]', 'title'); 292 params.append('_fields[]', 'link'); 293 params.append('_fields[]', 'id'); 294 params.append('_fields[]', 'acf.service_start'); 295 params.append('_fields[]', 'acf.service_description'); 296 params.append('_fields[]', 'acf.khadamati_service_id'); 297 298 if (state.search) { 299 params.set('search', state.search); 300 } 301 302 if (state.category) { 303 params.set('service_category', state.category); 304 } 305 306 $.ajax({ 307 url: '/wp-json/wp/v2/service?' + params.toString(), 308 method: 'GET', 309 dataType: 'json' 310 }) 311 .done(function (data, textStatus, jqXHR) { 312 var totalItems = parseInt(jqXHR.getResponseHeader('X-WP-Total') || '0', 10); 313 var totalPages = parseInt(jqXHR.getResponseHeader('X-WP-TotalPages') || '1', 10); 314 315 state.totalItems = isNaN(totalItems) ? 0 : totalItems; 316 state.totalPages = Math.max(1, isNaN(totalPages) ? 1 : totalPages); 317 318 renderResults(data); 319 }) 320 .fail(function (jqXHR, textStatus) { 321 console.error('[Services] Error:', textStatus, jqXHR.responseText); 322 setStatus(messages.requestFailed || 'Unable to load services.', true); 323 renderMessage(messages.requestFailed || 'Unable to load services.'); 324 $paginationWrap.addClass('hidden').removeClass('flex'); 325 }) 326 .always(function () { 327 setLoading(false); 328 }); 329 } 330 331 function submitSearch() { 332 state.search = String($search.val() || '').trim(); 333 state.category = String($category.val() || '').trim(); 334 state.page = 1; 335 fetchServices(); 336 } 337 338 $search.on('keydown', function (event) { 339 if (event.key !== 'Enter') { 340 return; 341 } 342 343 event.preventDefault(); 344 submitSearch(); 345 }); 346 347 $search.on('input', debounce(function () { 348 submitSearch(); 349 }, 250)); 350 351 $category.on('change', function () { 352 submitSearch(); 353 }); 354 355 $trigger.on('click', function (event) { 356 event.preventDefault(); 357 submitSearch(); 358 }); 359 360 $(document).on('click', '.js-services-pagination-page', function (event) { 361 var targetPage; 362 363 event.preventDefault(); 364 365 if (state.loading) { 366 return; 367 } 368 369 targetPage = parseInt($(this).attr('data-page') || '0', 10); 370 371 if (!targetPage || targetPage === state.page) { 372 return; 373 } 374 375 state.page = targetPage; 376 fetchServices(); 377 }); 378 379 $(document).on('click', '.js-service-bookmark', function (event) { 380 var $button = $(this); 381 var bookmarkKey = String($button.attr('data-bookmark-key') || ''); 382 var bookmarks; 383 var isSaved; 384 var label; 385 386 event.preventDefault(); 387 388 if (!bookmarkKey) { 389 return; 390 } 391 392 bookmarks = getBookmarks(); 393 isSaved = !$button.hasClass('checked'); 394 395 if (isSaved) { 396 bookmarks[bookmarkKey] = true; 397 } else { 398 delete bookmarks[bookmarkKey]; 399 } 400 401 setBookmarks(bookmarks); 402 $button.toggleClass('checked', isSaved); 403 $button.attr('aria-pressed', isSaved ? 'true' : 'false'); 404 label = isSaved ? (labels.saved || 'Service saved') : (labels.save || 'Save service'); 405 $button.attr('aria-label', label).attr('title', label); 406 }); 407 408 $first.on('click', function (event) { 409 event.preventDefault(); 410 411 if (state.loading || state.page === 1) { 412 return; 413 } 414 415 state.page = 1; 416 fetchServices(); 417 }); 418 419 $prev.on('click', function (event) { 420 event.preventDefault(); 421 422 if (state.loading || state.page === 1) { 423 return; 424 } 425 426 state.page -= 1; 427 fetchServices(); 428 }); 429 430 $next.on('click', function (event) { 431 event.preventDefault(); 432 433 if (state.loading || state.page >= state.totalPages) { 434 return; 435 } 436 437 state.page += 1; 438 fetchServices(); 439 }); 440 441 $last.on('click', function (event) { 442 event.preventDefault(); 443 444 if (state.loading || state.page === state.totalPages) { 445 return; 446 } 447 448 state.page = state.totalPages; 449 fetchServices(); 450 }); 451 452 function debounce(callback, wait) { 453 var timeoutId; 454 455 return function () { 456 var context = this; 457 var args = arguments; 458 459 clearTimeout(timeoutId); 460 timeoutId = setTimeout(function () {
461 callback.apply(context, args); 462 }, wait); 463 }; 464 } 465 466 fetchServices(); 467})(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.