1import { Controller } from "../stimulus.js"; 2import { COOKIE_NAME, DEFAULTS, SHOW_SPINNER_ACTION, VALID_TYPES, readFilterCookie, buildFilterParams } from 'searchFilterUtils'; 3 4const COOKIE_MAX_AGE = 365 * 24 * 60 * 60; 5const DATE_INPUT_RE = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/; 6 7const MONTH_NAMES = [ 8 'January', 'February', 'March', 'April', 'May', 'June', 9 'July', 'August', 'September', 'October', 'November', 'December', 10]; 11const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 12 13export default class extends Controller { 14 static targets = [ 15 'modal', 'spinner', 'queryInput', 'errorMessage', 16 'scopeSite', 'scopeNetwork', 17 'typePR', 'typeAgpWire', 'typeNews', 18 'searchInHeadlines', 'searchInAll', 19 'dateModeDuration', 'dateModeCustom', 20 'durationSelect', 21 'dateFromInput', 'dateToInput', 22 'dateFromBtn', 'dateToBtn', 23 'calendarDropdown', 24 'sortDate', 'sortRelevance', 25 ]; 26 27 #filters = { ...DEFAULTS }; 28 #closeListener = null; 29 #turboRenderHandler = null; 30 #showSpinnerHandler = null; 31 #fromDate = ''; 32 #toDate = ''; 33 #activeCalendarField = null; 34 #calendarOpen = false; 35 #calendarYear = new Date().getFullYear(); 36 #calendarMonth = new Date().getMonth(); 37 #calendarDocClick = null; 38 #calendarScrollHandler = null; 39 #calendarResizeHandler = null; 40 41 connect() { 42 this.#closeListener = () => this.#restoreScroll(); 43 this.modalTarget.addEventListener('close', this.#closeListener); 44 this.#loadFromCookie(); 45 this.#updateWarningBar(); 46 this.#turboRenderHandler = () => { 47 this.#updateWarningBar(); 48 this.#checkApiError(); 49 }; 50 document.addEventListener('turbo:render', this.#turboRenderHandler); 51 this.#showSpinnerHandler = () => this.showSpinner(); 52 document.addEventListener(`search:${SHOW_SPINNER_ACTION}`, this.#showSpinnerHandler); 53 this.#checkApiError(); 54 this.#normalizeUrl(); 55 } 56 57 disconnect() { 58 if (this.#closeListener) { 59 this.modalTarget.removeEventListener('close', this.#closeListener); 60 this.#closeListener = null; 61 } 62 if (this.#turboRenderHandler) { 63 document.removeEventListener('turbo:render', this.#turboRenderHandler); 64 this.#turboRenderHandler = null; 65 } 66 if (this.#showSpinnerHandler) { 67 document.removeEventListener(`search:${SHOW_SPINNER_ACTION}`, this.#showSpinnerHandler); 68 this.#showSpinnerHandler = null; 69 } 70 this.#closeCalendar(); 71 this.#restoreScroll(); 72 } 73 74 open(event) { 75 event?.preventDefault(); 76 const match = window.location.pathname.match(/^\/search\/(.+)$/); 77 if (match) this.queryInputTarget.value = decodeURIComponent(match[1]); 78 this.#applyUrlOverrides(); 79 this.#renderForm(); 80 document.body.classList.add('overflow-hidden'); 81 this.modalTarget.showModal(); 82 } 83 84 close() { 85 this.#closeCalendar(); 86 this.errorMessageTarget.textContent = ''; 87 this.modalTarget.close(); 88 } 89 90 clickOutside(event) { 91 if (event.target !== this.modalTarget) return; 92 const rect = this.modalTarget.getBoundingClientRect(); 93 const outside = event.clientX < rect.left || event.clientX > rect.right || 94 event.clientY < rect.top || event.clientY > rect.bottom; 95 if (outside) this.close(); 96 } 97 98 toggleType(event) { 99 const btn = event.currentTarget; 100 const otherSelected = this.#typeTargets.some(b => b !== btn && b.classList.contains('selected')); 101 if (btn.classList.contains('selected') && !otherSelected) return; 102 const isSelected = btn.classList.toggle('selected'); 103 btn.setAttribute('aria-pressed', String(isSelected)); 104 } 105 106 dialogEnter(event) { 107 const tag = event.target.tagName.toLowerCase(); 108 if (['input', 'select', 'button', 'textarea', 'a'].includes(tag)) return; 109 this.applyFilters(); 110 } 111 112 applyFilters() { 113 const query = this.queryInputTarget.value.trim(); 114 if (!query) { 115 this.close(); 116 return; 117 } 118 this.#readForm(); 119 if (this.#isDefault()) { 120 this.#clearCookie(); 121 } else { 122 this.#saveCookie(); 123 } 124 this.#updateWarningBar(); 125 this.close(); 126 document.activeElement?.blur(); 127 this.showSpinner(); 128 window.Turbo.visit('/search/' + encodeURIComponent(query) + this.#urlParam
128s()); 129 } 130 131 showSpinner() { 132 const el = this.spinnerTarget; 133 el.style.display = 'flex'; 134 const hide = () => { 135 el.style.display = 'none'; 136 document.removeEventListener('turbo:before-render', hide); 137 document.removeEventListener('turbo:before-stream-render', hide); 138 }; 139 document.addEventListener('turbo:before-render', hide); 140 document.addEventListener('turbo:before-stream-render', hide); 141 } 142 143 resetFilters() { 144 this.#resetState(); 145 this.#renderForm(); 146 this.#clearCookie(); 147 this.#updateWarningBar(); 148 } 149 150 reset(event) { 151 event?.preventDefault(); 152 this.#resetState(); 153 this.#clearCookie(); 154 this.#updateWarningBar(); 155 const match = window.location.pathname.match(/^\/search\/(.+)$/); 156 if (match) window.Turbo.visit('/search/' + match[1]); 157 } 158 159 toggleDateMode() { 160 const isCustom = this.dateModeCustomTarget.checked; 161 this.durationSelectTarget.disabled = isCustom; 162 this.dateFromInputTarget.disabled = !isCustom; 163 this.dateFromBtnTarget.disabled = !isCustom; 164 this.dateToInputTarget.disabled = !isCustom; 165 this.dateToBtnTarget.disabled = !isCustom; 166 if (!isCustom) this.#closeCalendar(); 167 } 168 169 // Calendar â per-field open/close 170 171 openFromCalendar() { 172 if (this.#calendarOpen && this.#activeCalendarField === 'from') return; 173 this.#openCalendar('from'); 174 } 175 176 openToCalendar() { 177 if (this.#calendarOpen && this.#activeCalendarField === 'to') return; 178 this.#openCalendar('to'); 179 } 180 181 toggleFromCalendar() { 182 if (this.#calendarOpen && this.#activeCalendarField === 'from') { 183 this.#closeCalendar(); 184 } else { 185 this.#openCalendar('from'); 186 } 187 } 188 189 toggleToCalendar() { 190 if (this.#calendarOpen && this.#activeCalendarField === 'to') { 191 this.#closeCalendar(); 192 } else { 193 this.#openCalendar('to'); 194 } 195 } 196 197 calendarPrev() { 198 this.#calendarMonth--; 199 if (this.#calendarMonth < 0) { 200 this.#calendarMonth = 11; 201 this.#calendarYear--; 202 } 203 this.#renderCalendar(); 204 } 205 206 calendarNext() { 207 this.#calendarMonth++; 208 if (this.#calendarMonth > 11) { 209 this.#calendarMonth = 0; 210 this.#calendarYear++; 211 } 212 this.#renderCalendar(); 213 } 214 215 calendarSelectDay(event) { 216 this.#setDate(this.#activeCalendarField, event.currentTarget.dataset.date); 217 this.#updateDateDisplay(); 218 this.#renderCalendar(); 219 } 220 221 calendarDone() { 222 this.#closeCalendar(); 223 } 224 225 calendarClear() { 226 if (this.#activeCalendarField === 'from') { 227 this.#fromDate = ''; 228 this.dateFromInputTarget.value = ''; 229 } else { 230 this.#toDate = ''; 231 this.dateToInputTarget.value = ''; 232 } 233 this.#renderCalendar(); 234 } 235 236 // Date input handlers 237 238 handleDateFromInput(event) { this.#handleDateInput('from', event); } 239 handleDateToInput(event) { this.#handleDateInput('to', event); } 240 241 dateInputEnter(event) { 242 event.preventDefault(); 243 event.stopPropagation(); 244 event.target.blur(); 245 if (this.#calendarOpen) this.#closeCalendar(); 246 } 247 248 blurDateFrom() { this.#blurDate('from'); } 249 blurDateTo() { this.#blurDate('to'); } 250 251 // Private 252 253 #handleDateInput(field, event) { 254 const input = field === 'from' ? this.dateFromInputTarget : this.dateToInputTarget; 255 this.#autoFormatDateInput(input, event); 256 const iso = this.#parseDateInput(input.value); 257 this.#setDate(field, iso); 258 if (iso && this.#calendarOpen && this.#activeCalendarField === field) { 259 const d = new Date(iso + 'T00:00:00'); 260 this.#calendarYear = d.getFullYear(); 261 this.#calendarMonth = d.getMonth(); 262 this.#renderCalendar(); 263 } 264 } 265 266 #blurDate(field) { 267 const input = field === 'from' ? this.dateFromInputTarget : this.dateToInputTarget; 268 const iso = this.#parseDateInput(input.value); 269 if (iso) input.value = this.#formatDateDisplay(iso); 270 this.#setDate(field, iso); 271 } 272 273 #setDate(field, iso) { 274 if (field === 'from') { 275 this.#fromDate = iso; 276 if (iso && this.#toDate && iso > this.#toDate) { 277 this.#toDate = iso; 278 this.dateToInputTarget.value = this.#formatDateDisplay(iso); 279 } 280 } else { 281 this.#toDate = iso; 282 if (iso && this.#fromDate && iso < this.#fromDate) { 283 this.#fromDate = iso; 284 this.dateFromInputTarget.value = this.#formatDateDisplay(iso); 285 } 286 } 287 } 288 289 #openCalendar(field) { 290 this.#activeCalendarField = field; 291 const date = field === 'from' ? this.#fromDate : this.#toDate; 292 if (date) { 293 const d = new Date(date + 'T00:00:00'); 294 this.#calendarYear = d.getFullYear(); 295 this.#calendarMonth = d.getMonth(); 296 } else { 297 const today = new Date(); 298 this.#calendarYear = today.getFullYear(); 299 this.#calendarMonth = today.getMonth(); 300 } 301 const el = this.calendarDropdownTarget;
302 this.#renderCalendar(); 303 el.classList.remove('hidden'); 304 this.#positionCalendar(); 305 this.#calendarOpen = true; 306 307 if (this.#calendarScrollHandler) { 308 this.modalTarget.removeEventListener('scroll', this.#calendarScrollHandler); 309 } 310 this.#calendarScrollHandler = () => this.#closeCalendar(); 311 this.modalTarget.addEventListener('scroll', this.#calendarScrollHandler); 312 313 if (this.#calendarDocClick) { 314 document.removeEventListener('click', this.#calendarDocClick); 315 } 316 this.#calendarDocClick = (e) => { 317 // Target may have been removed from DOM by #renderCalendar() before bubbling reached document 318 if (!document.contains(e.target)) return; 319 if (!el.contains(e.target) && 320 !this.dateFromInputTarget.contains(e.target) && 321 !this.dateFromBtnTarget.contains(e.target) && 322 !this.dateToInputTarget.contains(e.target) && 323 !this.dateToBtnTarget.contains(e.target)) { 324 this.#closeCalendar(); 325 } 326 }; 327 setTimeout(() => document.addEventListener('click', this.#calendarDocClick), 0); 328 329 if (this.#calendarResizeHandler) { 330 window.removeEventListener('resize', this.#calendarResizeHandler); 331 } 332 this.#calendarResizeHandler = () => this.#positionCalendar(); 333 window.addEventListener('resize', this.#calendarResizeHandler); 334 } 335 336 #closeCalendar() { 337 if (!this.#calendarOpen) return; 338 this.calendarDropdownTarget.classList.add('hidden'); 339 this.calendarDropdownTarget.innerHTML = ''; 340 this.#calendarOpen = false; 341 this.#activeCalendarField = null; 342 if (this.#calendarDocClick) { 343 document.removeEventListener('click', this.#calendarDocClick); 344 this.#calendarDocClick = null; 345 } 346 if (this.#calendarScrollHandler) { 347 this.modalTarget.removeEventListener('scroll', this.#calendarScrollHandler); 348 this.#calendarScrollHandler = null; 349 } 350 if (this.#calendarResizeHandler) { 351 window.removeEventListener('resize', this.#calendarResizeHandler); 352 this.#calendarResizeHandler = null; 353 } 354 } 355 356 #positionCalendar() { 357 const input = this.#activeCalendarField === 'from' 358 ? this.dateFromInputTarget 359 : this.dateToInputTarget; 360 const rect = input.getBoundingClientRect(); 361 const el = this.calendarDropdownTarget; 362 const left = Math.max(8, Math.min(rect.left, window.innerWidth - el.offsetWidth - 8)); 363 const spaceBelow = window.innerHeight - rect.bottom - 8; 364 const top = spaceBelow >= el.offsetHeight 365 ? rect.bottom + 8 366 : rect.top - el.offsetHeight - 8; 367 el.style.top = `${top}px`; 368 el.style.left = `${left}px`; 369 } 370 371 #renderCalendar() { 372 this.calendarDropdownTarget.innerHTML = this.#buildCalendarHTML(); 373 } 374 375 #buildCalendarHTML() { 376 return ` 377 <div class="adv-calendar"> 378 <div class="adv-calendar-months"> 379 ${this.#buildMonthGrid(this.#calendarYear, this.#calendarMonth)} 380 </div> 381 <div class="adv-calendar-footer"> 382 <button type="button" data-action="click->advanced-search#calendarDone" class="adv-cal-done-btn">Done</button> 383 <button type="button" data-action="click->advanced-search#calendarClear" class="adv-cal-clear-btn">Clear</button> 384 </div> 385 </div> 386 `; 387 } 388 389 #buildMonthGrid(year, month) { 390 const firstDow = new Date(year, month, 1).getDay(); 391 const daysInMonth = new Date(year, month + 1, 0).getDate(); 392 const daysInPrevMonth = new Date(year, month, 0).getDate(); 393 const trailingCount = (7 - ((firstDow + daysInMonth) % 7)) % 7; 394 395 const prevM = month === 0 ? 11 : month - 1; 396 const prevY = month === 0 ? year - 1 : year; 397 const nextM = month === 11 ? 0 : month + 1; 398 const nextY = month === 11 ? year + 1 : year; 399 400 const today = this.#isoDate(new Date()); 401 const btn = (d, dateStr, overflow) => { 402 const isFuture = dateStr > today; 403 const cls = this.#dayClass(dateStr); 404 return `<button type="button" class="adv-cal-day${overflow ? ' adv-cal-overflow' : ''}${cls ? ' ' + cls : ''}"${isFuture ? ' disabled' : ''} data-date="${dateStr}" data-action="click->advanced-search#calendarSelectDay">${d}</button>`; 405 }; 406
407 let cells = ''; 408 409 for (let i = firstDow - 1; i >= 0; i--) { 410 const d = daysInPrevMonth - i; 411 const dateStr = `${prevY}-${String(prevM + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; 412 cells += btn(d, dateStr, true); 413 } 414 415 for (let d = 1; d <= daysInMonth; d++) { 416 const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; 417 cells += btn(d, dateStr, false); 418 } 419 420 for (let d = 1; d <= trailingCount; d++) { 421 const dateStr = `${nextY}-${String(nextM + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; 422 cells += btn(d, dateStr, true); 423 } 424 425 const dowRow = DAY_NAMES.map(n => `<div class="adv-cal-dow">${n}</div>`).join(''); 426 427 return ` 428 <div class="adv-calendar-month"> 429 <div class="adv-cal-header"> 430 <button type="button" data-action="click->advanced-search#calendarPrev" class="adv-cal-nav" aria-label="Previous month"> 431 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 432 <path d="M10 12L6 8L10 4" stroke="#151515" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 433 </svg> 434 </button> 435 <span class="adv-cal-month-name">${MONTH_NAMES[month]} ${year}</span> 436 <button type="button" data-action="click->advanced-search#calendarNext" class="adv-cal-nav" aria-label="Next month"> 437 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 438 <path d="M6 4L10 8L6 12" stroke="#151515" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 439 </svg> 440 </button> 441 </div> 442 <div class="adv-cal-dow-row">${dowRow}</div> 443 <div class="adv-cal-grid">${cells}</div> 444 </div> 445 `; 446 } 447 448 #dayClass(dateStr) { 449 const activeDate = this.#activeCalendarField === 'from' ? this.#fromDate : this.#toDate; 450 return dateStr === activeDate ? 'adv-cal-selected' : ''; 451 } 452 453 #isoDate(date) { 454 return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; 455 } 456 457 #formatDateDisplay(dateStr) { 458 if (!dateStr) return ''; 459 const [y, m, d] = dateStr.split('-'); 460 return `${m}/${d}/${y}`; 461 } 462 463 #autoFormatDateInput(input, event) { 464 if (event?.inputType?.startsWith('delete')) return; 465 if (event?.inputType === 'insertFromPaste') { 466 const iso = this.#parseDateInput(input.value); 467 if (iso) input.value = this.#formatDateDisplay(iso); 468 return; 469 } 470 const caretPos = input.selectionStart; 471 const prev = input.value; 472 const rawDigits = prev.replace(/\D/g, ''); 473 474 let digits, targetDigitPos; 475 if (rawDigits.length > 8 && event.data?.length === 1 && /\d/.test(event.data)) { 476 // Full date â overwrite the digit at the cursor rather than shifting. 477 // event.data.length === 1 guarantees exactly one char was inserted, so caretPos - 1 is reliable. 478 const overwriteIndex = prev.slice(0, caretPos - 1).replace(/\D/g, '').length; 479 digits = (rawDigits.slice(0, overwriteIndex + 1) + rawDigits.slice(overwriteIndex + 2)).slice(0, 8); 480 targetDigitPos = overwriteIndex + 1; 481 } else { 482 digits = rawDigits.slice(0, 8); 483 targetDigitPos = prev.slice(0, caretPos).replace(/\D/g, '').length; 484 } 485 486 let formatted = digits; 487 if (digits.length > 2) formatted = `${digits.slice(0, 2)}/${digits.slice(2)}`; 488 if (digits.length > 4) formatted = `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`; 489 if (formatted === prev) return; 490 491 input.value = formatted; 492 let pos = 0, count = 0; 493 while (pos < formatted.length && count < targetDigitPos) { 494 if (formatted[pos] !== '/') count++; 495 pos++; 496 } 497 input.setSelectionRange(pos, pos); 498 } 499 500 #parseDateInput(value) { 501 if (!value) return ''; 502 const match = value.match(DATE_INPUT_RE); 503 if (!match) return ''; 504 const [, m, d, y] = match; 505 const year = parseInt(y), month = parseInt(m) - 1, day = parseInt(d); 506 const date = new Date(year, month, day); 507 if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return ''; 508 if (year < 1900) return ''; // news articles don't predate 1900; also prevents calendar NaN for very old years 509 const today = new Date(); 510 today.setHours(0, 0, 0, 0); 511 if (date > today) return ''; 512 return this.#isoDate(date); 513 } 514 515 #updateDateDisplay() { 516 this.dateFromInputTarget.value = this.#fromDate ? this.#formatDateDisplay(this.#fromDate) : ''; 517 this.dateToInputTarget.value = this.#toDate ? this.#formatDateDisplay(this.#toDate) : ''; 518 } 519 520 #checkApiError() { 521 const el = document.querySelector('[data-api-error]'); 522 if (!el) return; 523 const hasError = !!el.dataset.apiError; 524 el.removeAttribute('data-api-error'); 525 if (!hasError) return; 526 this.open(); 527 this.errorMessageTarget.textContent = 'Invalid search parameters. Please check your date range and try again.'; 528 } 529 530 #applyUrlOverrides() { 531 const params = new URLSearchParams(window.location.search); 532 const today = new Date().toISOString().slice(0, 10); 533 534 const scope = params.get('scope'); 535 if (params.has('scope')) this.#filters.scope = (scope === 'site' || scope === 'network') ? scope : DEFAULTS.scope; 536 537 const sort = params.get('sort'); 538 if (params.has('sort')) this.#filters.sortBy = (sort === 'date' || sort === 'relevance') ? sort : DEFAULTS.sortBy; 539 540 const searchIn = params.get('search_in'); 541 if (params.has('search_in')) this.#filters.searchIn = (searchIn === 'all' || searchIn === 'headlines') ? searchIn : DEFAULTS.searchIn; 542 543 const type = params.get('type'); 544 if (params.has('type')) { 545 const parsed = (type || '').split(',').map(t => t.trim()).filter(t => VALID_TYPES.has(t)); 546 this.#filters.types = parsed.length > 0 ? parsed : DEFAULTS.types; 547 } 548 549 const duration = params.get('duration'); 550 if (params.has('duration')) { 551 const validDurations = new Set([...this.durationSelectTarget.options].map(o =>
551 o.value).filter(Boolean)); 552 this.#filters.dateMode = 'duration'; 553 this.#filters.duration = validDurations.has(duration) ? duration : DEFAULTS.duration; 554 } 555 556 if (params.has('date_from') || params.has('date_to')) { 557 const rawFrom = params.get('date_from') || ''; 558 const rawTo = params.get('date_to') || ''; 559 this.#filters.dateMode = 'custom'; 560 this.#filters.dateFrom = rawFrom && rawFrom > today ? today : rawFrom; 561 this.#filters.dateTo = rawTo && rawTo > today ? today : rawTo; 562 } 563 } 564 565 #normalizeUrl() { 566 const params = new URLSearchParams(window.location.search); 567 if (!params.has('date_from') && !params.has('date_to')) return; 568 const today = new Date().toISOString().slice(0, 10); 569 const rawFrom = params.get('date_from') || ''; 570 const rawTo = params.get('date_to') || ''; 571 const cappedFrom = rawFrom && rawFrom > today ? today : rawFrom; 572 const cappedTo = rawTo && rawTo > today ? today : rawTo; 573 if (cappedFrom === rawFrom && cappedTo === rawTo) return; 574 if (cappedFrom) params.set('date_from', cappedFrom); else params.delete('date_from'); 575 if (cappedTo) params.set('date_to', cappedTo); else params.delete('date_to'); 576 history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`); 577 } 578 579 get #typeTargets() { 580 return [this.typeNewsTarget, this.typeAgpWireTarget, this.typePRTarget]; 581 } 582 583 #resetState() { 584 this.#fromDate = ''; 585 this.#toDate = ''; 586 this.#filters = { ...DEFAULTS }; 587 } 588 589 #restoreScroll() { 590 document.body.classList.remove('overflow-hidden'); 591 } 592 593 #readForm() { 594 const filters = this.#filters; 595 filters.types = []; 596 if (this.typeNewsTarget.classList.contains('selected')) filters.types.push('news'); 597 if (this.typeAgpWireTarget.classList.contains('selected')) filters.types.push('agp_style_article'); 598 if (this.typePRTarget.classList.contains('selected')) filters.types.push('press_release'); 599 filters.scope = this.scopeNetworkTarget.checked ? 'network' : 'site'; 600 filters.searchIn = this.searchInAllTarget.checked ? 'all' : 'headlines'; 601 filters.dateMode = this.dateModeDurationTarget.checked ? 'duration' : 'custom'; 602 filters.duration = this.durationSelectTarget.value; 603 filters.dateFrom = this.#fromDate; 604 filters.dateTo = this.#toDate; 605 filters.sortBy = this.sortRelevanceTarget.checked ? 'relevance' : 'date'; 606 } 607 608 #renderForm() { 609 const filters = this.#filters; 610 611 this.scopeNetworkTarget.checked = (filters.scope || DEFAULTS.scope) === 'network'; 612 this.scopeSiteTarget.checked = (filters.scope || DEFAULTS.scope) === 'site'; 613 614 const noTypeFilter = filters.types.length === 0; 615 const newsSelected = noTypeFilter || filters.types.includes('news'); 616 const agpWireSelected = noTypeFilter || filters.types.includes('agp_style_article'); 617 const prSelected = noTypeFilter || filters.types.includes('press_release'); 618 this.typeNewsTarget.classList.toggle('selected', newsSelected); 619 this.typeNewsTarget.setAttribute('aria-pressed', String(newsSelected)); 620 this.typeAgpWireTarget.classList.toggle('selected', agpWireSelected); 621 this.typeAgpWireTarget.setAttribute('aria-pressed', String(agpWireSelected)); 622 this.typePRTarget.classList.toggle('selected', prSelected); 623 this.typePRTarget.setAttribute('aria-pressed', String(prSelected)); 624 625 this.searchInAllTarget.checked = (filters.searchIn || DEFAULTS.searchIn) === 'all'; 626 this.searchInHeadlinesTarget.checked = (filters.searchIn || DEFAULTS.searchIn) === 'headlines'; 627 628 this.dateModeDurationTarget.checked = filters.dateMode === 'duration'; 629 this.dateModeCustomTarget.checked = filters.dateMode === 'custom'; 630 this.durationSelectTarget.value = filters.duration; 631 this.toggleDateMode(); 632 633 this.#fromDate = filters.dateFrom || ''; 634 this.#toDate = filters.dateTo || ''; 635 this.#updateDateDisplay(); 636 637 this.sortRelevanceTarget.checked = (filters.sortBy || DEFAULTS.sortBy) === 'relevance'; 638 this.sortDateTarget.checked = (filters.sortBy || DEFAULTS.sortBy) === 'date'; 639 } 640 641 #isDefault() { 642 const filters = this.#filters; 643 const typesAreDefault = filters.types.length === 0 || filters.types.length === VALID_TYPES.size; 644 return typesAreDefault && 645 (filters.scope === DEFAULTS.scope || !filters.scope) && 646 (filters.searchIn === DEFAULTS.searchIn || !filters.searchIn) && 647 filters.dateMode === DEFAULTS.dateMode && 648 filters.duration === DEFAULTS.duration && 649 !filters.dateFrom && !filters.dateTo && 650 (filters.sortBy === DEFAULTS.sortBy || !filters.sortBy); 651 } 652
653 #urlParams() { 654 const params = buildFilterParams(this.#filters); 655 new URLSearchParams(window.location.search).getAll('classifications[]') 656 .forEach(value => params.append('classifications[]', value)); 657 const queryString = params.toString(); 658 return queryString ? '?' + queryString : ''; 659 } 660 661 #saveCookie() { 662 const v = encodeURIComponent(JSON.stringify(this.#filters)); 663 document.cookie = `${COOKIE_NAME}=${v}; path=/; SameSite=Lax; max-age=${COOKIE_MAX_AGE}`; 664 } 665 666 #clearCookie() { 667 document.cookie = `${COOKIE_NAME}=; path=/; SameSite=Lax; max-age=0`; 668 } 669 670 #loadFromCookie() { 671 const saved = readFilterCookie(); 672 if (saved) { 673 this.#filters = { ...DEFAULTS, ...saved }; 674 } 675 } 676 677 #updateWarningBar() { 678 const bar = document.getElementById('advanced-search-warning'); 679 if (!bar) return; 680 bar.style.display = this.#isDefault() ? 'none' : 'flex'; 681 } 682}
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.