PageSourceSearch

https://internationalnewsledger.com/js/controllers/advancedSearchC…?v9e91e12689728780f01d6dd9a70eb5c9bae56d64

js internationalnewsledger.com collected 2026-09-29 11:35:47 UTC 24,800 bytes, 682 lines download raw bytes

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.