PageSourceSearch

https://sarhaan.com/wp-content/plugins/islamic-reads-quran-center/…/compat/reader-v1/assets/js/site-search.js

js sarhaan.com collected 2026-10-03 19:11:06 UTC 9,728 bytes, 96 lines download raw bytes

1/* Site-wide public search. No index writes, third-party browser calls, or private content. */
2(() => {
3 'use strict';
4 const copy = {
5  ar: {title:'البحث في الموقع',placeholder:'ابحث في جميع محتويات الموقع…',hint:'الكتب، القرآن وتفسيره، الحديث، المصاحف والتلاوات',search:'بحث',close:'إغلاق البحث',intro:'اكتب كلمتين أو حرفين على الأقل لبدء البحث.',loading:'جارٍ البحث…',empty:'لا توجد نتائج في هذا القسم.',error:'تعذّر البحث مؤقتًا. حاول مرة أخرى.',more:'عرض المزيد',retry:'إعادة المحاولة',content:'الكتب ومحتوى الموقع',quran:'القرآن وتفسيره وترجماته',hadeeth:'موسوعة الحديث',catalog:'السور والمصاحف والتلاوات',books:'كتاب',surahs:'سورة',mushafs:'مصحف',audio:'تلاوة',translation:'ترجمة',tafsir:'تفسير',glossary:'معاني الكلمات'},
6  en: {title:'Search the site',placeholder:'Search all site content…',hint:'Books, Quran, Hadeeth, Mushafs and recitations',search:'Search',close:'Close search',intro:'Enter at least two characters to search.',loading:'Searching…',empty:'No results in this section.',error:'Search is temporarily unavailable. Please retry.',more:'Show more',retry:'Retry',content:'Books & site content',quran:'Quran, Tafsir & translations',hadeeth:'Hadeeth Encyclopedia',catalog:'Surahs, Mushafs & recitations',books:'Book',surahs:'Surah',mushafs:'Mushaf',audio:'Recitation',translation:'Translation',tafsir:'Tafsir',glossary:'Word meanings'}
7 };
8 const icon = (kind) => {
9  const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 24 24');svg.setAttribute('aria-hidden','true');
10  svg.innerHTML=kind==='close'?'<path d="m6 6 12 12M6 18 18 6"/>':'<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>';
11  return svg;
12 };
13 function ready() {
14  const inner=document.querySelector('.irhp-site-header__inner');
15  if(!inner||inner.querySelector('.irqc-header-search')||!window.IRQCSiteSearch?.endpoint)return;
16  const endpoint=new URL(window.IRQCSiteSearch.endpoint,location.href);if(endpoint.origin!==location.origin)return;
17  const form=document.createElement('form');form.className='irqc-header-search';form.setAttribute('role','search');
18  const field=document.createElement('input');field.type='search';field.maxLength=80;field.autocomplete='off';
19  const submit=document.createElement('button');submit.type='submit';submit.append(icon('search'));form.append(field,submit);inner.append(form);
20  const dialog=document.createElement('dialog');dialog.className='irqc-site-search-dialog';dialog.setAttribute('aria-labelledby','irqc-site-search-title');
21  const head=document.createElement('div');head.className='irqc-site-search-head';
22  const title=document.createElement('h2');title.id='irqc-site-search-title';const close=document.createElement('button');close.type='button';close.className='irqc-site-search-close';close.append(icon('close'));head.append(title,close);
23  const search=document.createElement('form');search.className='irqc-site-search-query';search.setAttribute('role','search');
24  const input=document.createElement('input');input.type='search';input.maxLength=80;input.autocomplete='off';const go=document.createElement('button');go.type='submit';go.append(icon('search'));search.append(input,go);
25  const hint=document.createElement('p');hint.className='irqc-site-search-hint';
26  const groups=document.createElement('div');groups.className='irqc-site-search-groups';
27  dialog.append(head,search,hint,groups);document.body.append(dialog);
28  let labels=copy.en,controller=null,revision=0,timer=null,previousFocus=null;
29  const sections=new Map();
30  for(const scope of ['content','quran','hadeeth','catalog']){
31   const section=document.createElement('section');section.dataset.scope=scope;
32   const heading=document.createElement('h3'),list=document.createElement('div'),status=document.createElement('p'),more=document.createElement('button');
33   status.className='irqc-site-search-status';status.setAttribute('role','status');more.type='button';more.hidden=true;
34   section.append(heading,list,status,more);groups.append(section);
35   const state={scope,heading,list,status,more,cursor:null,count:0,ids:new Set(),busy:false,failed:false};sections.set(scope,state);
36   more.addEventListener('click',()=>load(state,revision,input.value.trim()));
37  }
38  function language(){
39   const lang=document.querySelector('[data-irqc-app][data-language]')?.dataset.language||document.documentElement.lang||'en';
40   labels=copy[lang.startsWith('ar')?'ar':'en'];const rtl=labels===copy.ar;
41   form.dir=dialog.dir=rtl?'rtl':'ltr';form.lang=dialog.lang=rtl?'ar':'en';title.textContent=labels.title;
42   for(const f of [field,input]){f.placeholder=labels.placeholder;f.setAttribute('aria-label',labels.title);}
43   for(const b of [submit,go])b.setAttribute('aria-label',labels.search);
44   close.setAttribute('aria-label',labels.close);hint.textContent=labels.hint;
45   sections.forEach(s=>{s.heading.textContent=labels[s.scope];s.more.textContent=labels.more;});
46  }
47  function stop(){clearTimeout(timer);controller?.abort();controller=null;revision++;}
48  function dismiss(){if(dialog.open)dialog.close();}
49  dialog.addEventListener('close',()=>{stop();document.body.classList.remove('irqc-site-search-open');if(previousFocus?.isConnected)previousFocus.focus({preventScroll:true});});
50  close.addEventListener('click',dismiss);
51  dialog.addEventListener('keydown',event=>{if(event.key==='Escape'){event.preventDefault();dismiss();}});
52  dialog.addEventListener('click',event=>{if(event.target!==dialog)return;const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>
52r.right||event.clientY<r.top||event.clientY>r.bottom)dismiss();});
53  function open(){if(dialog.open)return;language();previousFocus=document.activeElement;dialog.showModal();document.body.classList.add('irqc-site-search-open');input.value=field.value;input.focus({preventScroll:true});begin();}
54  form.addEventListener('submit',event=>{event.preventDefault();open();});
55  field.addEventListener('input',()=>{if(field.value.trim().length>=2)open();});
56  input.addEventListener('input',()=>{field.value=input.value;stop();timer=setTimeout(begin,300);});
57  search.addEventListener('submit',event=>{event.preventDefault();field.value=input.value;begin();});
58  function begin(){
59   stop();const q=input.value.trim(),mine=revision;controller=new AbortController();
60   sections.forEach(s=>{s.heading.textContent=labels[s.scope];s.list.replaceChildren();s.ids.clear();s.count=0;s.cursor=null;s.busy=false;s.failed=false;s.more.hidden=true;s.status.textContent=q.length<2?labels.intro:labels.loading;});
61   if(q.length<2)return;
62   sections.forEach(s=>load(s,mine,q));
63  }
64  async function load(state,mine,q){
65   if(state.busy||mine!==revision||!controller)return;
66   state.busy=true;state.more.hidden=true;state.status.textContent=labels.loading;
67   const signal=controller.signal;
68   try {
69    // Continue empty batches; never declare no results while sources remain unsearched.
70    do {
71     const url=new URL(endpoint);url.searchParams.set('q',q);url.searchParams.set('scope',state.scope);if(state.cursor)url.searchParams.set('cursor',state.cursor);
72     const response=await fetch(url,{signal,credentials:'same-origin',headers:{Accept:'application/json'}});
73     if(!response.ok)throw new Error('search');
74     const data=await response.json();if(mine!==revision||signal.aborted)return;
75     if(!Array.isArray(data.results)||data.results.length>12||!(data.next_cursor===null||typeof data.next_cursor==='string')||data.complete!==(data.next_cursor===null)||data.next_cursor===state.cursor&&data.next_cursor!==null)throw new Error('response');
76     for(const result of data.results){
77      if(typeof result.id!=='string'||typeof result.url!=='string'||typeof result.title!=='string'||state.ids.has(result.id))continue;
78      const target=new URL(result.url,location.href);if(target.origin!==location.origin||!['http:','https:'].includes(target.protocol))continue;
79      const link=document.createElement('a');link.href=target.href;link.className='irqc-site-search-result';
80      const meta=document.createElement('small');meta.textContent=(labels[result.section]||result.section||'')+(result.language?' · '+result.language:'');
81      const name=document.createElement('strong');name.dir='auto';name.textContent=result.title;link.append(meta,name);
82      if(result.text){const text=document.createElement('p');text.dir='auto';text.textContent=result.text;link.append(text);}
83      link.addEventListener('click',dismiss);state.list.append(link);state.ids.add(result.id);state.count++;
84     }
85     state.cursor=data.next_cursor;
86    }while(state.cursor&&state.count<6&&mine===revision&&!signal.aborted);
87    if(mine!==revision||signal.aborted)return;
88    state.failed=false;state.heading.textContent=labels[state.scope]+(state.count?' ('+state.count+')':'');state.more.textContent=labels.more;state.more.hidden=!state.cursor;state.status.textContent=state.count?'':labels.empty;
89   }catch(error){if(mine!==revision||signal.aborted)return;state.failed=true;state.status.textContent=labels.error;state.more.textContent=labels.retry;state.more.hidden=false;}
90   finally{if(mine===revision)state.busy=false;}
91  }
92  language();sections.forEach(s=>s.status.textContent=labels.intro);
93  document.addEventListener('irqc:navigation',()=>{dismiss();language();});
94 }
95 if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',ready);else ready();
96})();

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.