ما يحفظه النموذج
احفظ slug للجهة المختارة، لا اسمها ولا رابط شعارها: الاسم قد يُصحّح، والشعار قد يتغيّر، والمعرّف يبقى. واقرأ السجل حين تحتاج الشعار والاسم.
الطلب
brand-picker.js
const base = 'https://new-wasmenia-4zjd6l.cranl.net';let pending; // the request still in flight, if any/** Suggestions for what the user has typed so far. */export async function suggest(text) { const q = text.trim(); if (q.length < 2) return []; // shorter is answered empty and not counted pending?.abort(); // a newer keystroke makes the older answer useless pending = new AbortController(); const url = new URL('/api/v1/search/suggest', base); url.searchParams.set('q', q); const res = await fetch(url, { signal: pending.signal }); if (res.status === 429) { // Over 90 a minute from this address: wait, then try again. const wait = Number(res.headers.get('Retry-After') ?? 60); throw Object.assign(new Error('rate_limited'), { wait }); } if (!res.ok) throw new Error(`HTTP ${res.status}`); const { items } = await res.json(); return items.map((b) => ({ slug: b.slug, // what you store label: b.nameAr, secondary: b.nameEn, // may be null image: b.thumbUrl, // may be null: show the name alone verified: b.isVerified, }));}بين الحروف
لا تطلب مع كل حرف: انتظر قرابة 250 ملي ثانية بعد آخر حرف، وألغِ الطلب السابق حين يصل حرف جديد. هكذا يبقى المستخدم العادي بعيدًا عن حد الـ 90 في الدقيقة.
مع حقل الإدخال
let timer;input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(async () => { try { render(await suggest(input.value)); } catch (e) { if (e.name === 'AbortError') return; // replaced by a newer request if (e.message === 'rate_limited') return pauseFor(e.wait); showError(); } }, 250);});لوحة المفاتيح وقارئ الشاشة
- الحقل
role="combobox"والقائمةrole="listbox"، وكل نتيجةrole="option". - الأسهم تتنقل بين النتائج، وEnter يختار، وEsc يغلق القائمة.
aria-activedescendantيشير إلى النتيجة الحالية، وعدد النتائج يُعلن في منطقةaria-live.- نتيجة بلا
thumbUrlتعرض الاسم وحده.