IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging29/29-dars58 daqiqa
Mundarija (49)

JavaScript yakuniy loyiha: brauzerda vazifalar ro'yxati (vanilla JS)

Qisqacha: Bu darsda freymvorksiz, toza JavaScript'da vazifalar ro'yxati ilovasini noldan quramiz va GitHub Pages'ga chiqaramiz. Ilova vazifa qo'shadi, belgilaydi, tahrirlaydi, o'chiradi va filtrlaydi. Hammasi 09-qism qoliplarida: ma'lumot bitta holat obyektida, ekranni render() chizadi, ro'yxatga bitta delegation tinglovchisi, matn faqat textContent bilan. Ilova klaviatura va ekran o'quvchi bilan ham to'liq ishlaydi.

Bu darsda

  • 09-qism bilimlarini bitta ishlaydigan ilovaga yig'asiz: DOM, hodisalar, formalar, qulaylik va debugging.
  • CRUD ilovani holat → render qolipida, sakkiz bosqichda qurasiz.
  • Tahrirlash rejimini yasaysiz: Enter saqlaydi, Esc bekor qiladi, fokus to'g'ri joyga qaytadi.
  • Ilovani klaviatura, XSS va stack trace bilan tekshirasiz.
  • Loyihani vazifalar reposiga branch va commitlar bilan joylab, GitHub Pages'ga chiqarasiz.

Oldin bilishingiz kerak: Holatdan UI chizish, Event delegation, Forma qiymatlarini o'qish va submit, Dinamik UI'da qulaylik, Stack trace va xato turlari.

1. Nega bu kerak?

08-qism oxirida Aziz oylik byudjet dasturini yozgan edi (Yakuniy loyiha: oylik byudjet). U faqat terminalda ishlaydi. 09-qismda JavaScript brauzerga chiqdi: tugma, forma, jonli ro'yxat. Endi hammasini bitta haqiqiy ilovaga yig'amiz.

Vazifalar ro'yxati (to-do list) — bajarilishi kerak bo'lgan ishlar ro'yxati: "Non olish", "Onamga qo'ng'iroq qilish". Dasturchilar uni yangi bilimni sinash uchun eng ko'p yozadi. Sababi oddiy: kichik ilovada ma'lumot bilan ishlashning to'rtala asosiy amali bor. Ular CRUD deyiladi:

Harf Inglizcha Bizda
C Create — yaratish Vazifa qo'shish
R Read — o'qish Ro'yxatni ko'rsatish va filtrlash
U Update — yangilash Belgilash va tahrirlash
D Delete — o'chirish Vazifani o'chirish

Admin panel, do'kon savati, Telegram'dagi xabarlar — hammasi shu to'rt amalga quriladi. Vazifalar ilovasini to'g'ri yoza olsangiz, ularning ham "skeleti"ni bilasiz.

Loyiha vanilla JS da yoziladi — "hech qanday qo'shimchasiz oddiy muzqaymoq" kabi: freymvorksiz, kutubxonasiz, toza JavaScript. Keyinroq React kabi vositalarni (kursda alohida o'rganamiz) o'rganganingizda, ular qaysi ishni sizning o'rningizga qilayotganini aniq ko'rasiz.

vazifalar reposi kurs davomida o'sib boradi: 10-qismda modullarga bo'linadi, 11-qismda ma'lumotni saqlaydi, keyin TypeScript, React va testlar keladi. Bugun — uning birinchi versiyasi, v1.

2. Loyiha talablari

2.1 Ilova nima qiladi

  1. Qo'shish. Forma orqali yangi vazifa qo'shiladi. Bo'sh matn va ro'yxatda bor vazifa qabul qilinmaydi, xato o'zbekcha tushuntiriladi.
  2. Belgilash. Har vazifa yonida katak (checkbox): bajarildi yoki yo'q.
  3. Tahrirlash. «Tahrirlash» tugmasi qatorni maydonga aylantiradi. Enter yoki «Saqlash» saqlaydi, Esc yoki «Bekor qilish» eski matnni qaytaradi.
  4. O'chirish. «O'chirish» vazifani ro'yxatdan olib tashlaydi.
  5. Filtr. "Hammasi", "Faol" va "Bajarilgan" tugmalari.
  6. Hisoblagich. "2 ta vazifa qoldi" — ekran o'quvchi uni o'zgarganda o'qiydi.

2.2 Qoidalar

  • Holat → render. Ma'lumot faqat holat obyektida. Har o'zgarish: holatni o'zgartirish, keyin render() (Holatdan UI chizish darsidagi beshta qoida).
  • Delegation. Ro'yxatdagi hamma tugma uchun ul ga bitta click tinglovchisi (Event delegation).
  • Xavfsiz matn. Foydalanuvchi yozgan matn faqat textContent bilan ekranga chiqadi (XSS xavfi).
  • Klaviatura va ekran o'quvchi. Har amal sichqonchasiz bajariladi, fokus hech qachon yo'qolmaydi (Dinamik UI'da qulaylik).
  • Faqat o'tilgan narsa. Ro'yxatni filtrlash for...of va if bilan — massivning filter metodini 10-qismda o'rganamiz.

Diqqat: v1 ma'lumotni saqlamaydi. Sahifani yangilasangiz, ro'yxat boshlang'ich holatiga qaytadi. Bu ataylab: brauzer xotirasi (localStorage) hali o'tilmagan. Uni 11-qismda aynan shu ilovaga qo'shamiz. Holat bitta obyektda turgani uchun o'shanda bu ish bir necha qator bo'ladi.

2.3 Fayllar

Loyiha — yangi, alohida repo. Tuzilma CSS ulash darsidagi kelishuv bo'yicha:

text
vazifalar/
├── index.html
├── README.md
└── assets/
    ├── css/
    │   └── vazifalar.css
    └── js/
        └── vazifalar.js

Yo'llar nisbiy, skript <head> da defer bilan (Brauzer muhiti). Bu darsdagi jonli oynalar esa tashqi faylni ocha olmaydi. Shuning uchun ularda CSS <style> ichida, JavaScript <script> ichida turadi. Sizning loyihangizda ular alohida fayllarda bo'ladi.

Tekshirib ko'ring: Nega "nechta vazifa qoldi" degan son holat ichida saqlanmaydi?

Javob

U — hosila ma'lumot: har doim holat.vazifalar dan sanab olish mumkin. Alohida saqlansa, har belgilash va o'chirishda ikki joyni o'zgartirish kerak bo'ladi. Bittasi unutilsa, ekran "yolg'on gapiradi" — Holatdan UI chizish darsining «Nega bu kerak?» bo'limidagi xato.

3. Reja: ma'lumot qanday aylanadi

Kod yozishdan oldin — reja. Diagrammada strelkalar bir tomonga aylanadi. Hodisa tinglovchiga keladi, tinglovchi holatni o'zgartiruvchi funksiyani chaqiradi, u esa render() ni. Fokus va e'lon render() dan keyin qo'yiladi, chunki render() eski elementlarni almashtiradi:

flowchart TD
  H["Hodisa: submit, click, keydown"] --> T["Tinglovchi"]
  T --> O["O'zgartiruvchi: vazifaQosh..."]
  O --> S[("holat")]
  S --> R["render()"]
  R --> D["DOM: ro'yxat, hisob"]
  T --> F["fokusla, elonQil"]
  D -.->|"foydalanuvchi ko'radi"| H

Ish sakkiz bosqichda boradi:

# Bosqich Natija Tayanch dars
1 Repo va skelet HTML, CSS, birinchi commit Brauzer muhiti
2 Holat va render Ro'yxat holatdan chiziladi State → render
3 Qo'shish Forma, tekshiruv, xato xabari submit
4 Belgilash va o'chirish Bitta tinglovchi, fokus Delegation
5 Tahrirlash Enter, Esc, fokus qaytadi Klaviatura
6 Filtr va hisoblagich for...of, aria-live Qulaylik
7 Sifat XSS, klaviatura, debug Stack trace
8 Deploy login.github.io/vazifalar Pages

Har bosqichda avval o'zingiz yozib ko'ring, keyin yechim bilan solishtiring. Har bosqich oxiridagi jonli oynada shu paytgacha yozilgan butun kod bor — uni bosib, sinab ko'ring.

4. 1-bosqich: repo va skelet

4.1 Repo

Loyiha portfolio'dan alohida yashaydi. Git qismidagi kelishuv: repolar kurs/ ichida, kurs/ ning o'zida git init yo'q. Git Bash'da:

bash
cd ~/kurs
mkdir -p vazifalar/assets/css vazifalar/assets/js
cd vazifalar
git init
code .

4.2 HTML

index.html — sahifaning hamma "doimiy" qismi. Ro'yxat bo'sh: uni JavaScript chizadi.

html
<!DOCTYPE html>
<html lang="uz">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Vazifalar — kichik vazifalar ro'yxati</title>
  <meta name="description"
        content="Vanilla JavaScript'da yozilgan vazifalar ro'yxati.">
  <link rel="stylesheet" href="assets/css/vazifalar.css">
  <script src="assets/js/vazifalar.js" defer></script>
</head>
<body>
  <main>
    <h1>Vazifalar</h1>
    <form id="yangi-forma" novalidate>
      <label for="yangi-matn">Yangi vazifa</label>
      <div class="qator">
        <input id="yangi-matn" name="matn" required
               maxlength="100" autocomplete="off">
        <button type="submit">Qo'shish</button>
      </div>
      <p id="yangi-matn-xato" class="xato" role="status"></p>
    </form>
    <div id="filtrlar" role="group" aria-label="Filtr">
      <button type="button" data-filtr="hammasi"
              aria-pressed="true">Hammasi</button>
      <button type="button" data-filtr="faol"
              aria-pressed="false">Faol</button>
      <button type="button" data-filtr="bajarilgan"
              aria-pressed="false">Bajarilgan</button>
    </div>
    <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
    <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
    <p id="bosh-xabar" hidden></p>
    <p id="hisob" aria-live="polite"></p>
    <p id="elon" role="status"></p>
    <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
      holatiga qaytadi.</small></p>
  </main>
</body>
</html>

Qaysi atribut nima uchun:

  • novalidate — brauzer o'z pufagini chiqarmaydi, xatoni o'zimiz o'zbekcha ko'rsatamiz. required va maxlength="100" esa qoladi: ekran o'quvchi "majburiy" deydi, 100 dan ortiq belgi yozib bo'lmaydi (Constraint Validation API).
  • #yangi-matn-xato — xato xabari uchun joy, oldindan bo'sh turadi. role="status" tufayli ekran o'quvchi xabarni o'zi o'qiydi (Jonli hududlar).
  • aria-pressed — filtr tugmasi bosilgan holatdami. Malika NVDA bilan "Faol, tugma, bosilgan" deb eshitadi.
  • tabindex="-1" sarlavhada — oxirgi vazifa o'chganda fokusni shu yerga qo'yamiz.
  • aria-live="polite" — hisoblagich jonli hudud: matni o'zgarsa, ekran o'quvchi navbati bilan o'qiydi. #elon dagi role="status" ham xuddi shunday ishlaydi. U qisqa xabarlar uchun: "«Non olish» o'chirildi."

4.3 CSS

assets/css/vazifalar.css — to'liq matni «6-bosqich» oxiridagi «To'liq fayllar» ichida. Rang o'zgaruvchilari portfolio namunasidan. Har tugma kamida 44 px balandlikda — telefonda barmoq bilan bosish qulay bo'lsin. Fokus halqasi :focus-visible bilan ko'rinadi.

Natija — hali jonsiz skelet:

html
<style>
  :root {
    --rang-matn: #1f2a24;
    --rang-fon: #f7faf8;
    --rang-asosiy: #1f6f4a;
    --rang-chegara: #c9d6cf;
    --rang-ikkinchi: #5b6660;
    --rang-xato: #b3261e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--rang-matn);
    background: var(--rang-fon);
  }
  main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
  h1 { margin-top: 0; color: var(--rang-asosiy); }
  h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
  button, input { font: inherit; }
  button {
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rang-chegara);
    border-radius: 0.5rem;
    background: #fff;
    color: var(--rang-matn);
  }
  button[aria-pressed="true"] {
    border-color: var(--rang-asosiy);
    background: var(--rang-asosiy);
    color: #fff;
  }
  :focus-visible {
    outline: 3px solid var(--rang-asosiy);
    outline-offset: 2px;
  }
  .qator { display: flex; gap: 0.5rem; }
  #yangi-matn, #tahrir-maydon {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.5rem;
    border: 2px solid var(--rang-ikkinchi);
    border-radius: 0.5rem;
  }
  [aria-invalid="true"] { border-color: var(--rang-xato); }
  .xato {
    min-height: 1.5em;
    margin: 0.25rem 0 0;
    color: var(--rang-xato);
  }
  #filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  #royxat { margin: 0; padding: 0; list-style: none; }
  #royxat li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rang-chegara);
  }
  #royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
  #royxat [type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bajarildi label {
    color: var(--rang-ikkinchi);
    text-decoration: line-through;
  }
  #hisob { font-weight: bold; }
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="elon" role="status"></p>
  <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi.</small></p>
</main>

Maydonga "Non olish" deb yozib, Enter bosing. Oyna pastida yashil " Forma tekshiruvdan o'tdi" yozuvi chiqadi — bu jonli oynaning yordamchisi (Forma qiymatlarini o'qish). Haqiqiy saytda esa sahifa qayta yuklanardi va manzil satrida ?matn=Non+olish paydo bo'lardi. JavaScript hali formani ushlamayapti — buni 3-bosqichda qilamiz.

Birinchi commit. Commit xabari — kursdagi kelishuv: soha, keyin buyruq maylidagi fe'l (Yaxshi commit):

bash
git add .
git commit -m "Vazifalar: sahifa skeletini yarat"

5. 2-bosqich: holat va render

5.1 Vazifa

Har bosqichni alohida branch'da qiling (Branch): git switch -c feature/render. Keyin vazifalar.js ga yozing:

  1. holat obyekti: uchta namuna vazifa (id, matn, bajarildi) va keyingiId.
  2. oddiyQator(vazifa) — bitta li: katak, uning label i, «Tahrirlash» va «O'chirish» tugmalari.
  3. render() — ro'yxatni holatdan to'liq chizadi va "N ta vazifa qoldi" deb yozadi.
Yechim
js
const holat = {
  vazifalar: [
    { id: 1, matn: "Non olish", bajarildi: false },
    { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
    { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
  ],
  keyingiId: 4,
};

const royxat = document.getElementById("royxat");
const hisob = document.getElementById("hisob");

function faollarSoni() {
  let soni = 0;
  for (const vazifa of holat.vazifalar) {
    if (!vazifa.bajarildi) {
      soni++;
    }
  }
  return soni;
}

function tugmaYasa(amal, matn, nomi) {
  const tugma = document.createElement("button");
  tugma.type = "button";
  tugma.dataset.amal = amal;
  tugma.textContent = matn;
  tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
  return tugma;
}

function oddiyQator(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  li.classList.toggle("bajarildi", vazifa.bajarildi);
  const katak = document.createElement("input");
  katak.type = "checkbox";
  katak.id = `vazifa-${vazifa.id}`;
  katak.dataset.amal = "almashtir";
  katak.checked = vazifa.bajarildi;
  const yorliq = document.createElement("label");
  yorliq.htmlFor = katak.id;
  yorliq.textContent = vazifa.matn;
  li.append(
    katak,
    yorliq,
    tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
    tugmaYasa("ochir", "O'chirish", vazifa.matn),
  );
  return li;
}

function matnniYoz(element, matn) {
  if (element.textContent !== matn) {
    element.textContent = matn;
  }
}

function render() {
  const quti = document.createDocumentFragment();
  for (const vazifa of holat.vazifalar) {
    quti.append(oddiyQator(vazifa));
  }
  royxat.replaceChildren(quti);
  matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
}

render();

Asosiy qarorlar:

  • Katak (checkbox) + label. "Bajarildi" — ha/yo'q savoli, unga tayyor element bor. Space uni o'zi belgilaydi, NVDA "belgilangan" yoki "belgilanmagan" deydi, label ni bosish ham katakni bosadi. Bularning hammasi tekin (Tanlash maydonlari).
  • aria-label tugmalarda. Ekranda uchta «O'chirish» bor. Malika Tab bilan yurganda "O'chirish, tugma" ni uch marta eshitadi — qaysi biri ekanini bilmaydi. aria-label nomni "O'chirish: Non olish" qiladi. Ko'rinadigan so'z nomning boshida qoladi — ovoz bilan boshqaradigan foydalanuvchi "O'chirish ni bos" desa ham ishlaydi. setAttribute matnni HTML deb o'qimaydi, shuning uchun bu yo'l ham XSS'dan xavfsiz.
  • data-amal va data-id. Tugma nima qilishini va qaysi vazifaga tegishli ekanini aytadi — 4-bosqichdagi delegation shularni o'qiydi.
  • classList.toggle("bajarildi", shart) — ikkinchi argument class'ni majburlaydi (classList).
  • matnniYoz — matn haqiqatan o'zgargandagina yozadi. Hisoblagich jonli hudud: bir xil matnni qayta yozish ba'zi ekran o'quvchilarda ortiqcha e'lonni keltirib chiqaradi.
html
<style>
  :root {
    --rang-matn: #1f2a24;
    --rang-fon: #f7faf8;
    --rang-asosiy: #1f6f4a;
    --rang-chegara: #c9d6cf;
    --rang-ikkinchi: #5b6660;
    --rang-xato: #b3261e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--rang-matn);
    background: var(--rang-fon);
  }
  main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
  h1 { margin-top: 0; color: var(--rang-asosiy); }
  h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
  button, input { font: inherit; }
  button {
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rang-chegara);
    border-radius: 0.5rem;
    background: #fff;
    color: var(--rang-matn);
  }
  button[aria-pressed="true"] {
    border-color: var(--rang-asosiy);
    background: var(--rang-asosiy);
    color: #fff;
  }
  :focus-visible {
    outline: 3px solid var(--rang-asosiy);
    outline-offset: 2px;
  }
  .qator { display: flex; gap: 0.5rem; }
  #yangi-matn, #tahrir-maydon {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.5rem;
    border: 2px solid var(--rang-ikkinchi);
    border-radius: 0.5rem;
  }
  [aria-invalid="true"] { border-color: var(--rang-xato); }
  .xato {
    min-height: 1.5em;
    margin: 0.25rem 0 0;
    color: var(--rang-xato);
  }
  #filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  #royxat { margin: 0; padding: 0; list-style: none; }
  #royxat li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rang-chegara);
  }
  #royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
  #royxat [type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bajarildi label {
    color: var(--rang-ikkinchi);
    text-decoration: line-through;
  }
  #hisob { font-weight: bold; }
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="elon" role="status"></p>
  <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi.</small></p>
</main>
<script>
  // vazifalar.js — vazifalar ro'yxati v1 (09-qism yakuniy loyihasi)

  // 1. Holat — yagona haqiqat manbai
  const holat = {
    vazifalar: [
      { id: 1, matn: "Non olish", bajarildi: false },
      { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
      { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
    ],
    keyingiId: 4,
  };

  // 2. Sahifa elementlari
  const royxat = document.getElementById("royxat");
  const hisob = document.getElementById("hisob");

  // 3. Hisoblovchi funksiyalar: faqat qiymat qaytaradi
  function faollarSoni() {
    let soni = 0;
    for (const vazifa of holat.vazifalar) {
      if (!vazifa.bajarildi) {
        soni++;
      }
    }
    return soni;
  }

  // 4. Chizish: holatdan ekranga
  function tugmaYasa(amal, matn, nomi) {
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.dataset.amal = amal;
    tugma.textContent = matn;
    tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
    return tugma;
  }

  function oddiyQator(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    li.classList.toggle("bajarildi", vazifa.bajarildi);
    const katak = document.createElement("input");
    katak.type = "checkbox";
    katak.id = `vazifa-${vazifa.id}`;
    katak.dataset.amal = "almashtir";
    katak.checked = vazifa.bajarildi;
    const yorliq = document.createElement("label");
    yorliq.htmlFor = katak.id;
    yorliq.textContent = vazifa.matn;
    li.append(
      katak,
      yorliq,
      tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
      tugmaYasa("ochir", "O'chirish", vazifa.matn),
    );
    return li;
  }

  function matnniYoz(element, matn) {
    if (element.textContent !== matn) {
      element.textContent = matn;
    }
  }

  function render() {
    const quti = document.createDocumentFragment();
    for (const vazifa of holat.vazifalar) {
      quti.append(oddiyQator(vazifa));
    }
    royxat.replaceChildren(quti);
    matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
  }

  render();
  console.log(royxat.children); // DEBUG
  console.log(hisob.textContent); // DEBUG
</script>

Konsolda:

text
HTMLCollection(3) [li, li.bajarildi, li]
2 ta vazifa qoldi

Oxirgi ikki qator — vaqtinchalik tekshiruv, // DEBUG belgisi bilan (Debugging metodologiyasi). Faylingizda ularni commit'dan oldin o'chiring.

Endi "Non olish" katagini bosing. Belgi qo'yildi, lekin pastda hamon "2 ta vazifa qoldi". Katakni brauzer o'zi o'zgartirdi, holat esa bundan bexabar — ekran va ma'lumot ajraldi. Buni 4-bosqichda tuzatamiz.

bash
git add assets/js/vazifalar.js
git commit -m "Render: vazifalarni holatdan chiz"
git switch main
git merge feature/render

6. 3-bosqich: vazifa qo'shish

6.1 Vazifa

Branch: feature/qoshish. Yozing:

  1. xatoniTop(matn, vazifalar, ozId) — toza funksiya: xato bo'lsa matnini, bo'lmasa null qaytaradi. Ikki qoida: bo'sh matn va katta-kichik harfdan qat'i nazar bir xil vazifa. ozId — tekshiruvda o'tkazib yuboriladigan vazifa (tahrirda kerak bo'ladi).
  2. vazifaQosh(matn) — holatga yangi vazifa, keyin render().
  3. Formaning submit tinglovchisi: preventDefault, trim, tekshiruv, xato yoki qo'shish.
  4. Xato ko'rinishi input va change darsidagidek: aria-invalid va aria-describedby. Mehmon tuzata boshlaganda xato darhol yo'qolsin.

Ishora: "ro'yxatda bormi?" degan savolga some javob beradi (Massivda qidirish).

Yechim
js
const forma = document.getElementById("yangi-forma");
const maydon = document.getElementById("yangi-matn");
const maydonXato = document.getElementById("yangi-matn-xato");
const elon = document.getElementById("elon");

function xatoniTop(matn, vazifalar, ozId) {
  if (matn === "") {
    return "Vazifa matnini yozing.";
  }
  const kichik = matn.toLowerCase();
  const takror = vazifalar.some(
    (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
  );
  if (takror) {
    return "Bu vazifa ro'yxatda allaqachon bor.";
  }
  return null;
}

function vazifaQosh(matn) {
  holat.vazifalar.push({
    id: holat.keyingiId,
    matn,
    bajarildi: false,
  });
  holat.keyingiId++;
  render();
}

function elonQil(matn) {
  elon.textContent = matn;
}

function xatoniKorsat(xabar) {
  maydonXato.textContent = xabar;
  maydon.setAttribute("aria-invalid", "true");
  maydon.setAttribute("aria-describedby", maydonXato.id);
}

function xatoniTozala() {
  maydonXato.textContent = "";
  maydon.removeAttribute("aria-invalid");
  maydon.removeAttribute("aria-describedby");
}

forma.addEventListener("submit", (event) => {
  event.preventDefault();
  elonQil("");
  const matn = maydon.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, null);
  if (xato !== null) {
    xatoniKorsat(xato);
    maydon.focus();
    return;
  }
  xatoniTozala();
  vazifaQosh(matn);
  forma.reset();
  elonQil(`«${matn}» qo'shildi.`);
});

maydon.addEventListener("input", () => {
  if (maydon.getAttribute("aria-invalid") !== "true") {
    return;
  }
  const matn = maydon.value.trim();
  if (xatoniTop(matn, holat.vazifalar, null) === null) {
    xatoniTozala();
  }
});

Nimaga e'tibor bering:

  • xatoniTop ekranga tegmaydi. U 08-qismdagi xatoSababi ning egizagi (Yakuniy loyiha: byudjet): kirish — matn, chiqish — xato yoki null. Uni ham qo'shishda, ham tahrirda ishlatamiz.
  • trim() dan keyin tekshirish. Uch probel — bo'sh vazifa.
  • Fokus maydonda qoladi. Forma render() chizadigan joydan tashqarida, shuning uchun maydon qayta yaratilmaydi. Mehmon bir necha vazifani ketma-ket yozib, Enter bosib boraveradi.
  • "Kech ayblash, tez kechirish". Xato faqat yuborishda chiqadi, tuzatilishi bilan input uni o'chiradi (input va change).
html
<style>
  :root {
    --rang-matn: #1f2a24;
    --rang-fon: #f7faf8;
    --rang-asosiy: #1f6f4a;
    --rang-chegara: #c9d6cf;
    --rang-ikkinchi: #5b6660;
    --rang-xato: #b3261e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--rang-matn);
    background: var(--rang-fon);
  }
  main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
  h1 { margin-top: 0; color: var(--rang-asosiy); }
  h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
  button, input { font: inherit; }
  button {
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rang-chegara);
    border-radius: 0.5rem;
    background: #fff;
    color: var(--rang-matn);
  }
  button[aria-pressed="true"] {
    border-color: var(--rang-asosiy);
    background: var(--rang-asosiy);
    color: #fff;
  }
  :focus-visible {
    outline: 3px solid var(--rang-asosiy);
    outline-offset: 2px;
  }
  .qator { display: flex; gap: 0.5rem; }
  #yangi-matn, #tahrir-maydon {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.5rem;
    border: 2px solid var(--rang-ikkinchi);
    border-radius: 0.5rem;
  }
  [aria-invalid="true"] { border-color: var(--rang-xato); }
  .xato {
    min-height: 1.5em;
    margin: 0.25rem 0 0;
    color: var(--rang-xato);
  }
  #filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  #royxat { margin: 0; padding: 0; list-style: none; }
  #royxat li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rang-chegara);
  }
  #royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
  #royxat [type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bajarildi label {
    color: var(--rang-ikkinchi);
    text-decoration: line-through;
  }
  #hisob { font-weight: bold; }
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="elon" role="status"></p>
  <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi.</small></p>
</main>
<script>
  // vazifalar.js — vazifalar ro'yxati v1 (09-qism yakuniy loyihasi)

  // 1. Holat — yagona haqiqat manbai
  const holat = {
    vazifalar: [
      { id: 1, matn: "Non olish", bajarildi: false },
      { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
      { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
    ],
    keyingiId: 4,
  };

  // 2. Sahifa elementlari
  const forma = document.getElementById("yangi-forma");
  const maydon = document.getElementById("yangi-matn");
  const maydonXato = document.getElementById("yangi-matn-xato");
  const royxat = document.getElementById("royxat");
  const hisob = document.getElementById("hisob");
  const elon = document.getElementById("elon");

  // 3. Hisoblovchi funksiyalar: faqat qiymat qaytaradi
  function xatoniTop(matn, vazifalar, ozId) {
    if (matn === "") {
      return "Vazifa matnini yozing.";
    }
    const kichik = matn.toLowerCase();
    const takror = vazifalar.some(
      (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
    );
    if (takror) {
      return "Bu vazifa ro'yxatda allaqachon bor.";
    }
    return null;
  }

  function faollarSoni() {
    let soni = 0;
    for (const vazifa of holat.vazifalar) {
      if (!vazifa.bajarildi) {
        soni++;
      }
    }
    return soni;
  }

  // 4. Chizish: holatdan ekranga
  function tugmaYasa(amal, matn, nomi) {
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.dataset.amal = amal;
    tugma.textContent = matn;
    tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
    return tugma;
  }

  function oddiyQator(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    li.classList.toggle("bajarildi", vazifa.bajarildi);
    const katak = document.createElement("input");
    katak.type = "checkbox";
    katak.id = `vazifa-${vazifa.id}`;
    katak.dataset.amal = "almashtir";
    katak.checked = vazifa.bajarildi;
    const yorliq = document.createElement("label");
    yorliq.htmlFor = katak.id;
    yorliq.textContent = vazifa.matn;
    li.append(
      katak,
      yorliq,
      tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
      tugmaYasa("ochir", "O'chirish", vazifa.matn),
    );
    return li;
  }

  function matnniYoz(element, matn) {
    if (element.textContent !== matn) {
      element.textContent = matn;
    }
  }

  function render() {
    const quti = document.createDocumentFragment();
    for (const vazifa of holat.vazifalar) {
      quti.append(oddiyQator(vazifa));
    }
    royxat.replaceChildren(quti);
    matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
  }

  // 5. Holatni o'zgartiruvchi funksiyalar: holat → render()
  function vazifaQosh(matn) {
    holat.vazifalar.push({
      id: holat.keyingiId,
      matn,
      bajarildi: false,
    });
    holat.keyingiId++;
    render();
  }

  // 6. Fokus va e'lon: render'dan keyin
  function elonQil(matn) {
    elon.textContent = matn;
  }

  function xatoniKorsat(xabar) {
    maydonXato.textContent = xabar;
    maydon.setAttribute("aria-invalid", "true");
    maydon.setAttribute("aria-describedby", maydonXato.id);
  }

  function xatoniTozala() {
    maydonXato.textContent = "";
    maydon.removeAttribute("aria-invalid");
    maydon.removeAttribute("aria-describedby");
  }

  // 7. Hodisalar: har joyda bitta tinglovchi
  forma.addEventListener("submit", (event) => {
    event.preventDefault();
    elonQil("");
    const matn = maydon.value.trim();
    const xato = xatoniTop(matn, holat.vazifalar, null);
    if (xato !== null) {
      xatoniKorsat(xato);
      maydon.focus();
      return;
    }
    xatoniTozala();
    vazifaQosh(matn);
    forma.reset();
    elonQil(`«${matn}» qo'shildi.`);
  });

  maydon.addEventListener("input", () => {
    if (maydon.getAttribute("aria-invalid") !== "true") {
      return;
    }
    const matn = maydon.value.trim();
    if (xatoniTop(matn, holat.vazifalar, null) === null) {
      xatoniTozala();
    }
  });

  render();
</script>

Sinab ko'ring:

  • Bo'sh maydonda Enter — ostida qizil "Vazifa matnini yozing.", maydon ramkasi qizardi.
  • "Kitob o'qish" deb yozing — birinchi harfdayoq xato yo'qoladi. Enter — ro'yxat oxirida yangi vazifa, "3 ta vazifa qoldi", pastda "«Kitob o'qish» qo'shildi.", maydon tozalandi va fokus unda.
  • "non OLISH" ni qo'shib ko'ring — "Bu vazifa ro'yxatda allaqachon bor."

Bo'sh joylarni to'ldiring. " Non olish ".trim() natijasidagi belgilar soni — [:9]. Formaning sahifani almashtirishini to'xtatadigan metod — [:preventDefault].

Commit: Forma: vazifa qo'shishni ula.

7. 4-bosqich: belgilash va o'chirish — bitta tinglovchi

7.1 Vazifa

Branch: feature/belgilash-ochirish. Ro'yxatga bitta click tinglovchisi qo'ying. U closest("[data-amal]") bilan nishonni topsin va switch bilan ikki amalni bajarsin: almashtir (katak) va ochir. Har amaldan keyin fokus yo'qolmasin:

  • belgilashdan keyin — o'sha vazifaning yangi katagiga;
  • o'chirishdan keyin — keyingi (yo'q bo'lsa oldingi) qatorning «O'chirish» tugmasiga;
  • ro'yxat bo'shab qolsa — "Ro'yxat" sarlavhasiga.
Yechim
js
const sarlavha = document.getElementById("royxat-sarlavha");

function vazifaniTop(id) {
  return holat.vazifalar.find((v) => v.id === id);
}

function vazifaniAlmashtir(id) {
  const vazifa = vazifaniTop(id);
  vazifa.bajarildi = !vazifa.bajarildi;
  render();
}

function vazifaniOchir(id) {
  const orin = holat.vazifalar.findIndex((v) => v.id === id);
  holat.vazifalar.splice(orin, 1);
  render();
}

function fokusla(id, amal, zaxiraId) {
  const top = (i) =>
    royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
  const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
  nishon.focus();
}

function qoshniId(id) {
  const qator = royxat.querySelector(`[data-id="${id}"]`);
  const qoshni =
    qator.nextElementSibling ?? qator.previousElementSibling;
  return qoshni === null ? null : Number(qoshni.dataset.id);
}

royxat.addEventListener("click", (event) => {
  const nishon = event.target.closest("[data-amal]");
  if (nishon === null || !royxat.contains(nishon)) {
    return;
  }
  elonQil("");
  const amal = nishon.dataset.amal;
  const id = Number(nishon.closest("li").dataset.id);
  const zaxira = qoshniId(id);
  switch (amal) {
    case "almashtir":
      vazifaniAlmashtir(id);
      fokusla(id, "almashtir", zaxira);
      break;
    case "ochir":
      elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
      vazifaniOchir(id);
      fokusla(zaxira, "ochir", null);
      break;
  }
});

Kodning to'rt nozik joyi:

  • Number(...). dataset dan kelgan qiymat doim satr: "1". find ichidagi v.id === id esa 1 === "1" ni solishtiradi — false. Number siz kod yiqiladi. Bu xatoni 7-bosqichda ataylab qilib, stack trace bilan topamiz.
  • label ni bosish. Mehmon vazifa matnini bossa, brauzer avval label ga, keyin katakka click yuboradi. label da data-amal yo'q — tinglovchi birinchisini jimgina o'tkazib yuboradi, ikkinchisini bajaradi. Bitta bosish — bitta belgilash.
  • Fokus render() dan keyin. render() eski katakni o'chirib, yangisini yaratadi. Fokus eski element bilan ketadi (Holatdan UI chizish darsidagi «Fokus yo'qoladi» narxi). fokusla uni yangi elementga qaytaradi. top(id) ?? top(zaxiraId) ?? sarlavha — "o'zi, bo'lmasa qo'shnisi, bo'lmasa sarlavha" (?? operatori).
  • Qo'shni oldindan topiladi. qoshniId o'chirishdan oldin chaqiriladi: keyin qator yo'q bo'ladi (Dinamik UI'da qulaylik darsidagi qoida).

Holat ham, fokus ham to'g'ri. Endi ikkalasini jonli oynada sinab ko'ring:

html
<style>
  :root {
    --rang-matn: #1f2a24;
    --rang-fon: #f7faf8;
    --rang-asosiy: #1f6f4a;
    --rang-chegara: #c9d6cf;
    --rang-ikkinchi: #5b6660;
    --rang-xato: #b3261e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--rang-matn);
    background: var(--rang-fon);
  }
  main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
  h1 { margin-top: 0; color: var(--rang-asosiy); }
  h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
  button, input { font: inherit; }
  button {
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rang-chegara);
    border-radius: 0.5rem;
    background: #fff;
    color: var(--rang-matn);
  }
  button[aria-pressed="true"] {
    border-color: var(--rang-asosiy);
    background: var(--rang-asosiy);
    color: #fff;
  }
  :focus-visible {
    outline: 3px solid var(--rang-asosiy);
    outline-offset: 2px;
  }
  .qator { display: flex; gap: 0.5rem; }
  #yangi-matn, #tahrir-maydon {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.5rem;
    border: 2px solid var(--rang-ikkinchi);
    border-radius: 0.5rem;
  }
  [aria-invalid="true"] { border-color: var(--rang-xato); }
  .xato {
    min-height: 1.5em;
    margin: 0.25rem 0 0;
    color: var(--rang-xato);
  }
  #filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  #royxat { margin: 0; padding: 0; list-style: none; }
  #royxat li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rang-chegara);
  }
  #royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
  #royxat [type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bajarildi label {
    color: var(--rang-ikkinchi);
    text-decoration: line-through;
  }
  #hisob { font-weight: bold; }
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="elon" role="status"></p>
  <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi.</small></p>
</main>
<script>
  // vazifalar.js — vazifalar ro'yxati v1 (09-qism yakuniy loyihasi)

  // 1. Holat — yagona haqiqat manbai
  const holat = {
    vazifalar: [
      { id: 1, matn: "Non olish", bajarildi: false },
      { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
      { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
    ],
    keyingiId: 4,
  };

  // 2. Sahifa elementlari
  const forma = document.getElementById("yangi-forma");
  const maydon = document.getElementById("yangi-matn");
  const maydonXato = document.getElementById("yangi-matn-xato");
  const sarlavha = document.getElementById("royxat-sarlavha");
  const royxat = document.getElementById("royxat");
  const hisob = document.getElementById("hisob");
  const elon = document.getElementById("elon");

  // 3. Hisoblovchi funksiyalar: faqat qiymat qaytaradi
  function xatoniTop(matn, vazifalar, ozId) {
    if (matn === "") {
      return "Vazifa matnini yozing.";
    }
    const kichik = matn.toLowerCase();
    const takror = vazifalar.some(
      (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
    );
    if (takror) {
      return "Bu vazifa ro'yxatda allaqachon bor.";
    }
    return null;
  }

  function faollarSoni() {
    let soni = 0;
    for (const vazifa of holat.vazifalar) {
      if (!vazifa.bajarildi) {
        soni++;
      }
    }
    return soni;
  }

  function vazifaniTop(id) {
    return holat.vazifalar.find((v) => v.id === id);
  }

  // 4. Chizish: holatdan ekranga
  function tugmaYasa(amal, matn, nomi) {
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.dataset.amal = amal;
    tugma.textContent = matn;
    tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
    return tugma;
  }

  function oddiyQator(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    li.classList.toggle("bajarildi", vazifa.bajarildi);
    const katak = document.createElement("input");
    katak.type = "checkbox";
    katak.id = `vazifa-${vazifa.id}`;
    katak.dataset.amal = "almashtir";
    katak.checked = vazifa.bajarildi;
    const yorliq = document.createElement("label");
    yorliq.htmlFor = katak.id;
    yorliq.textContent = vazifa.matn;
    li.append(
      katak,
      yorliq,
      tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
      tugmaYasa("ochir", "O'chirish", vazifa.matn),
    );
    return li;
  }

  function matnniYoz(element, matn) {
    if (element.textContent !== matn) {
      element.textContent = matn;
    }
  }

  function render() {
    const quti = document.createDocumentFragment();
    for (const vazifa of holat.vazifalar) {
      quti.append(oddiyQator(vazifa));
    }
    royxat.replaceChildren(quti);
    matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
  }

  // 5. Holatni o'zgartiruvchi funksiyalar: holat → render()
  function vazifaQosh(matn) {
    holat.vazifalar.push({
      id: holat.keyingiId,
      matn,
      bajarildi: false,
    });
    holat.keyingiId++;
    render();
  }

  function vazifaniAlmashtir(id) {
    const vazifa = vazifaniTop(id);
    vazifa.bajarildi = !vazifa.bajarildi;
    render();
  }

  function vazifaniOchir(id) {
    const orin = holat.vazifalar.findIndex((v) => v.id === id);
    holat.vazifalar.splice(orin, 1);
    render();
  }

  // 6. Fokus va e'lon: render'dan keyin
  function fokusla(id, amal, zaxiraId) {
    const top = (i) =>
      royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
    const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
    nishon.focus();
  }

  function qoshniId(id) {
    const qator = royxat.querySelector(`[data-id="${id}"]`);
    const qoshni =
      qator.nextElementSibling ?? qator.previousElementSibling;
    return qoshni === null ? null : Number(qoshni.dataset.id);
  }

  function elonQil(matn) {
    elon.textContent = matn;
  }

  function xatoniKorsat(xabar) {
    maydonXato.textContent = xabar;
    maydon.setAttribute("aria-invalid", "true");
    maydon.setAttribute("aria-describedby", maydonXato.id);
  }

  function xatoniTozala() {
    maydonXato.textContent = "";
    maydon.removeAttribute("aria-invalid");
    maydon.removeAttribute("aria-describedby");
  }

  // 7. Hodisalar: har joyda bitta tinglovchi
  forma.addEventListener("submit", (event) => {
    event.preventDefault();
    elonQil("");
    const matn = maydon.value.trim();
    const xato = xatoniTop(matn, holat.vazifalar, null);
    if (xato !== null) {
      xatoniKorsat(xato);
      maydon.focus();
      return;
    }
    xatoniTozala();
    vazifaQosh(matn);
    forma.reset();
    elonQil(`«${matn}» qo'shildi.`);
  });

  maydon.addEventListener("input", () => {
    if (maydon.getAttribute("aria-invalid") !== "true") {
      return;
    }
    const matn = maydon.value.trim();
    if (xatoniTop(matn, holat.vazifalar, null) === null) {
      xatoniTozala();
    }
  });

  royxat.addEventListener("click", (event) => {
    const nishon = event.target.closest("[data-amal]");
    if (nishon === null || !royxat.contains(nishon)) {
      return;
    }
    elonQil("");
    const amal = nishon.dataset.amal;
    const id = Number(nishon.closest("li").dataset.id);
    const zaxira = qoshniId(id);
    switch (amal) {
      case "almashtir":
        vazifaniAlmashtir(id);
        fokusla(id, "almashtir", zaxira);
        break;
      case "ochir":
        elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
        vazifaniOchir(id);
        fokusla(zaxira, "ochir", null);
        break;
    }
  });

  render();
</script>

Sinab ko'ring:

  • "Non olish" katagini bosing — matn ustidan chiziladi, "1 ta vazifa qoldi". Fokus katakda qoldi. Endi hisob ham, holat ham ekran bilan bir xil.
  • "Onamga qo'ng'iroq qilish" katagiga Tab bilan keling va Space bosing — xuddi shunday, sichqonchasiz.
  • "DOM darsini takrorlash" yonidagi «O'chirish» — qator yo'qoladi, pastda "«DOM darsini takrorlash» o'chirildi.", fokus esa keyingi qatorning «O'chirish» tugmasida.
  • «Tahrirlash» hali hech narsa qilmaydi — switch da uning case i yo'q.

Tekshirib ko'ring: Delegation o'rniga oddiyQator ichida har tugmaga addEventListener qo'ysak ham ilova ishlardi. Nega bu yomonroq?

Javob

Har render() hamma qatorni yangidan yaratadi — demak har bosishda hamma tinglovchi ham qayta yaratiladi. Tinglovchilar soni vazifalar sonidan uch baravar ko'p bo'ladi. Delegation'da tinglovchi bitta, u ul da turadi va ul hech qachon qayta chizilmaydi (Event delegation).

Commit: Ro'yxat: belgilash va o'chirishni delegation bilan ula.

8. 5-bosqich: tahrirlash rejimi

8.1 Qaysi qator tahrirda — bu ham holat

Tahrirlash ikki ko'rinishli qator: oddiy (katak va matn) va tahrir (maydon va ikki tugma). Qaysi qator hozir tahrirda — bu ham UI holati, xuddi filtr kabi. Uni holatga qo'shamiz: tahrirId: null. render() shu qiymatga qarab qatorni u yoki bu ko'rinishda chizadi.

stateDiagram-v2
  [*] --> Oddiy
  Oddiy --> Tahrir: Tahrirlash
  Tahrir --> Oddiy: Enter yoki Saqlash
  Tahrir --> Oddiy: Esc yoki Bekor qilish
  Tahrir --> Tahrir: matn xato

Diagrammaga qarang: tahrirdan ikki chiqish yo'li bor, ikkalasi ham klaviaturada va tugmada. Telefon klaviaturasida Esc yo'q — Aziz tahrirni bekor qilish uchun «Bekor qilish» tugmasini bosadi.

8.2 Vazifa

Branch: feature/tahrirlash.

  1. tahrirQatori(vazifa) — maydon (id="tahrir-maydon", ichida eski matn) va «Saqlash», «Bekor qilish» tugmalari.
  2. render(): vazifa.id === holat.tahrirId bo'lsa — tahrirQatori, aks holda oddiyQator.
  3. «Tahrirlash» bosilganda — fokus maydonga, matn belgilangan holda (select()).
  4. Saqlash: xatoniTop bilan tekshirish (o'zini hisobga olmasdan), keyin fokus o'sha vazifaning «Tahrirlash» tugmasiga.
  5. Bekor qilish: holat o'zgarmaydi, fokus yana «Tahrirlash» ga.
  6. Maydonda Enter — saqlash, Esc — bekor qilish (Klaviatura hodisalari). isComposing paytida tegmang.
Yechim

Holatga tahrirId: null qo'shing. render() dagi sikl ichi:

js
if (vazifa.id === holat.tahrirId) {
  quti.append(tahrirQatori(vazifa));
} else {
  quti.append(oddiyQator(vazifa));
}

Yangi funksiyalar:

js
function tahrirQatori(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  const kiritma = document.createElement("input");
  kiritma.id = "tahrir-maydon";
  kiritma.value = vazifa.matn;
  kiritma.maxLength = 100;
  kiritma.setAttribute("aria-label", "Vazifa matni");
  li.append(
    kiritma,
    tugmaYasa("saqla", "Saqlash", vazifa.matn),
    tugmaYasa("bekor", "Bekor qilish", vazifa.matn),
  );
  return li;
}

function tahrirniBoshla(id) {
  holat.tahrirId = id;
  render();
}

function matnniAlmashtir(id, matn) {
  vazifaniTop(id).matn = matn;
  holat.tahrirId = null;
  render();
}

function tahrirniYop() {
  holat.tahrirId = null;
  render();
}

function tahrirgaOt() {
  const kiritma = document.getElementById("tahrir-maydon");
  kiritma.focus();
  kiritma.select();
}

function tahrirniSaqla() {
  const kiritma = document.getElementById("tahrir-maydon");
  const id = holat.tahrirId;
  const matn = kiritma.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, id);
  if (xato !== null) {
    kiritma.setAttribute("aria-invalid", "true");
    kiritma.focus();
    elonQil(xato);
    return;
  }
  matnniAlmashtir(id, matn);
  fokusla(id, "tahrirla", null);
  elonQil("O'zgarish saqlandi.");
}

function tahrirniBekorQil() {
  const id = holat.tahrirId;
  tahrirniYop();
  fokusla(id, "tahrirla", null);
}

royxat.addEventListener("keydown", (event) => {
  if (event.target.id !== "tahrir-maydon" || event.isComposing) {
    return;
  }
  if (event.key === "Enter") {
    event.preventDefault();
    tahrirniSaqla();
  } else if (event.key === "Escape") {
    tahrirniBekorQil();
  }
});

click tinglovchisiga — switch dan oldin va ichiga:

js
if (amal !== "saqla" && amal !== "bekor") {
  holat.tahrirId = null;
}
switch (amal) {
  // almashtir va ochir — avvalgidek
  case "tahrirla":
    tahrirniBoshla(id);
    tahrirgaOt();
    break;
  case "saqla":
    tahrirniSaqla();
    break;
  case "bekor":
    tahrirniBekorQil();
    break;
}

Qarorlarning sababi:

  • keydown ham delegation. Tahrir maydoni har render() da yangidan yaratiladi, shuning uchun tinglovchi ul da turadi va event.target.id bilan "bu tahrir maydonimi?" deb so'raydi.
  • isComposing. Ba'zi tillarda harf bir necha tugma bilan "yig'iladi". O'sha payt bosilgan Enter harfni tasdiqlaydi, vazifani saqlamasligi kerak (Klaviatura hodisalari).
  • Bekor qilish holatga tegmaydi. Maydondagi yangi matn holatga hech qachon yozilmagan. render() eski matnni holatdan oladi — "eski matnni tiklash" uchun alohida kod kerak emas.
  • Boshqa amal tahrirni yopadi. saqla va bekor dan boshqa har amal avval tahrirId ni tozalaydi — nega, pastdagi savolda.
  • Fokus qaytadi. Yopilgan narsadan fokus uni ochgan tugmaga qaytadi — Dinamik UI'da qulaylik darsidagi ikkinchi qoida.
html
<style>
  :root {
    --rang-matn: #1f2a24;
    --rang-fon: #f7faf8;
    --rang-asosiy: #1f6f4a;
    --rang-chegara: #c9d6cf;
    --rang-ikkinchi: #5b6660;
    --rang-xato: #b3261e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--rang-matn);
    background: var(--rang-fon);
  }
  main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
  h1 { margin-top: 0; color: var(--rang-asosiy); }
  h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
  button, input { font: inherit; }
  button {
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rang-chegara);
    border-radius: 0.5rem;
    background: #fff;
    color: var(--rang-matn);
  }
  button[aria-pressed="true"] {
    border-color: var(--rang-asosiy);
    background: var(--rang-asosiy);
    color: #fff;
  }
  :focus-visible {
    outline: 3px solid var(--rang-asosiy);
    outline-offset: 2px;
  }
  .qator { display: flex; gap: 0.5rem; }
  #yangi-matn, #tahrir-maydon {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.5rem;
    border: 2px solid var(--rang-ikkinchi);
    border-radius: 0.5rem;
  }
  [aria-invalid="true"] { border-color: var(--rang-xato); }
  .xato {
    min-height: 1.5em;
    margin: 0.25rem 0 0;
    color: var(--rang-xato);
  }
  #filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  #royxat { margin: 0; padding: 0; list-style: none; }
  #royxat li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rang-chegara);
  }
  #royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
  #royxat [type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bajarildi label {
    color: var(--rang-ikkinchi);
    text-decoration: line-through;
  }
  #hisob { font-weight: bold; }
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="elon" role="status"></p>
  <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi.</small></p>
</main>
<script>
  // vazifalar.js — vazifalar ro'yxati v1 (09-qism yakuniy loyihasi)

  // 1. Holat — yagona haqiqat manbai
  const holat = {
    vazifalar: [
      { id: 1, matn: "Non olish", bajarildi: false },
      { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
      { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
    ],
    keyingiId: 4,
    tahrirId: null,
  };

  // 2. Sahifa elementlari
  const forma = document.getElementById("yangi-forma");
  const maydon = document.getElementById("yangi-matn");
  const maydonXato = document.getElementById("yangi-matn-xato");
  const sarlavha = document.getElementById("royxat-sarlavha");
  const royxat = document.getElementById("royxat");
  const hisob = document.getElementById("hisob");
  const elon = document.getElementById("elon");

  // 3. Hisoblovchi funksiyalar: faqat qiymat qaytaradi
  function xatoniTop(matn, vazifalar, ozId) {
    if (matn === "") {
      return "Vazifa matnini yozing.";
    }
    const kichik = matn.toLowerCase();
    const takror = vazifalar.some(
      (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
    );
    if (takror) {
      return "Bu vazifa ro'yxatda allaqachon bor.";
    }
    return null;
  }

  function faollarSoni() {
    let soni = 0;
    for (const vazifa of holat.vazifalar) {
      if (!vazifa.bajarildi) {
        soni++;
      }
    }
    return soni;
  }

  function vazifaniTop(id) {
    return holat.vazifalar.find((v) => v.id === id);
  }

  // 4. Chizish: holatdan ekranga
  function tugmaYasa(amal, matn, nomi) {
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.dataset.amal = amal;
    tugma.textContent = matn;
    tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
    return tugma;
  }

  function oddiyQator(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    li.classList.toggle("bajarildi", vazifa.bajarildi);
    const katak = document.createElement("input");
    katak.type = "checkbox";
    katak.id = `vazifa-${vazifa.id}`;
    katak.dataset.amal = "almashtir";
    katak.checked = vazifa.bajarildi;
    const yorliq = document.createElement("label");
    yorliq.htmlFor = katak.id;
    yorliq.textContent = vazifa.matn;
    li.append(
      katak,
      yorliq,
      tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
      tugmaYasa("ochir", "O'chirish", vazifa.matn),
    );
    return li;
  }

  function tahrirQatori(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    const kiritma = document.createElement("input");
    kiritma.id = "tahrir-maydon";
    kiritma.value = vazifa.matn;
    kiritma.maxLength = 100;
    kiritma.setAttribute("aria-label", "Vazifa matni");
    li.append(
      kiritma,
      tugmaYasa("saqla", "Saqlash", vazifa.matn),
      tugmaYasa("bekor", "Bekor qilish", vazifa.matn),
    );
    return li;
  }

  function matnniYoz(element, matn) {
    if (element.textContent !== matn) {
      element.textContent = matn;
    }
  }

  function render() {
    const quti = document.createDocumentFragment();
    for (const vazifa of holat.vazifalar) {
      if (vazifa.id === holat.tahrirId) {
        quti.append(tahrirQatori(vazifa));
      } else {
        quti.append(oddiyQator(vazifa));
      }
    }
    royxat.replaceChildren(quti);
    matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
  }

  // 5. Holatni o'zgartiruvchi funksiyalar: holat → render()
  function vazifaQosh(matn) {
    holat.vazifalar.push({
      id: holat.keyingiId,
      matn,
      bajarildi: false,
    });
    holat.keyingiId++;
    render();
  }

  function vazifaniAlmashtir(id) {
    const vazifa = vazifaniTop(id);
    vazifa.bajarildi = !vazifa.bajarildi;
    render();
  }

  function vazifaniOchir(id) {
    const orin = holat.vazifalar.findIndex((v) => v.id === id);
    holat.vazifalar.splice(orin, 1);
    render();
  }

  function tahrirniBoshla(id) {
    holat.tahrirId = id;
    render();
  }

  function matnniAlmashtir(id, matn) {
    vazifaniTop(id).matn = matn;
    holat.tahrirId = null;
    render();
  }

  function tahrirniYop() {
    holat.tahrirId = null;
    render();
  }

  // 6. Fokus va e'lon: render'dan keyin
  function fokusla(id, amal, zaxiraId) {
    const top = (i) =>
      royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
    const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
    nishon.focus();
  }

  function qoshniId(id) {
    const qator = royxat.querySelector(`[data-id="${id}"]`);
    const qoshni =
      qator.nextElementSibling ?? qator.previousElementSibling;
    return qoshni === null ? null : Number(qoshni.dataset.id);
  }

  function elonQil(matn) {
    elon.textContent = matn;
  }

  function xatoniKorsat(xabar) {
    maydonXato.textContent = xabar;
    maydon.setAttribute("aria-invalid", "true");
    maydon.setAttribute("aria-describedby", maydonXato.id);
  }

  function xatoniTozala() {
    maydonXato.textContent = "";
    maydon.removeAttribute("aria-invalid");
    maydon.removeAttribute("aria-describedby");
  }

  function tahrirgaOt() {
    const kiritma = document.getElementById("tahrir-maydon");
    kiritma.focus();
    kiritma.select();
  }

  function tahrirniSaqla() {
    const kiritma = document.getElementById("tahrir-maydon");
    const id = holat.tahrirId;
    const matn = kiritma.value.trim();
    const xato = xatoniTop(matn, holat.vazifalar, id);
    if (xato !== null) {
      kiritma.setAttribute("aria-invalid", "true");
      kiritma.focus();
      elonQil(xato);
      return;
    }
    matnniAlmashtir(id, matn);
    fokusla(id, "tahrirla", null);
    elonQil("O'zgarish saqlandi.");
  }

  function tahrirniBekorQil() {
    const id = holat.tahrirId;
    tahrirniYop();
    fokusla(id, "tahrirla", null);
  }

  // 7. Hodisalar: har joyda bitta tinglovchi
  forma.addEventListener("submit", (event) => {
    event.preventDefault();
    elonQil("");
    const matn = maydon.value.trim();
    const xato = xatoniTop(matn, holat.vazifalar, null);
    if (xato !== null) {
      xatoniKorsat(xato);
      maydon.focus();
      return;
    }
    xatoniTozala();
    vazifaQosh(matn);
    forma.reset();
    elonQil(`«${matn}» qo'shildi.`);
  });

  maydon.addEventListener("input", () => {
    if (maydon.getAttribute("aria-invalid") !== "true") {
      return;
    }
    const matn = maydon.value.trim();
    if (xatoniTop(matn, holat.vazifalar, null) === null) {
      xatoniTozala();
    }
  });

  royxat.addEventListener("click", (event) => {
    const nishon = event.target.closest("[data-amal]");
    if (nishon === null || !royxat.contains(nishon)) {
      return;
    }
    elonQil("");
    const amal = nishon.dataset.amal;
    const id = Number(nishon.closest("li").dataset.id);
    const zaxira = qoshniId(id);
    if (amal !== "saqla" && amal !== "bekor") {
      holat.tahrirId = null;
    }
    switch (amal) {
      case "almashtir":
        vazifaniAlmashtir(id);
        fokusla(id, "almashtir", zaxira);
        break;
      case "ochir":
        elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
        vazifaniOchir(id);
        fokusla(zaxira, "ochir", null);
        break;
      case "tahrirla":
        tahrirniBoshla(id);
        tahrirgaOt();
        break;
      case "saqla":
        tahrirniSaqla();
        break;
      case "bekor":
        tahrirniBekorQil();
        break;
    }
  });

  royxat.addEventListener("keydown", (event) => {
    if (event.target.id !== "tahrir-maydon" || event.isComposing) {
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      tahrirniSaqla();
    } else if (event.key === "Escape") {
      tahrirniBekorQil();
    }
  });

  render();
</script>

Sinab ko'ring:

  • "Onamga qo'ng'iroq qilish" yonidagi «Tahrirlash» — qator maydonga aylanadi, matn belgilangan, fokus maydonda. "Dadamga qo'ng'iroq qilish" deb yozing va Enter bosing — yangi matn, pastda "O'zgarish saqlandi.", fokus shu qatorning «Tahrirlash» tugmasida.
  • Yana Enter — tahrir qaytadan ochiladi. Biror narsa yozib, Esc bosing — eski matn joyida, fokus yana «Tahrirlash» da.
  • Maydonni tozalab «Saqlash» ni bosing — "Vazifa matnini yozing.", tahrir ochiq qoladi.

Bo'sh joyni to'ldiring. Esc tugmasi bosilganda event.key ning qiymati — .

Tekshirib ko'ring: Aziz "Non olish" ni tahrirlayapti, maydonga yangi matn yozdi, lekin saqlamay «O'chirish» ni boshqa qatorda bosdi. Yozgan matni nima bo'ladi?

Javob

Yo'qoladi va "Non olish" eski matni bilan qaytadi. click tinglovchisi saqla va bekor dan boshqa har amalda avval tahrirId ni tozalaydi. Yangi matn holatga hech qachon yozilmagan, render() esa holatdagi matnni chizadi. Bu ataylab tanlangan xulq: chala tahrir "jimgina saqlanib" qolmasin.

Commit: Tahrir: Enter va Esc bilan tahrirlash rejimini qo'sh.

9. 6-bosqich: filtr, hisoblagich va bo'sh holat

9.1 Vazifa

Branch: feature/filtr.

  1. Holatga filtr: "hammasi". mosmi(vazifa, filtr) va korinadiganlar() — for...of va if bilan (Holatdan UI chizish darsidagi qolip).
  2. render() faqat ko'rinadigan vazifalarni chizsin, filtr tugmalarining aria-pressed ini holatdan qo'ysin.
  3. Ro'yxat bo'sh bo'lsa, #bosh-xabar chiqsin — har filtr uchun o'z matni.
  4. #filtrlar ga bitta tinglovchi.
  5. "Bajarilgan" filtrida yangi vazifa qo'shilsa, filtr "Hammasi" ga o'tsin — aks holda mehmon qo'shgan vazifasini ko'rmaydi.
Yechim
js
const boshMatnlar = {
  hammasi: "Hozircha vazifa yo'q. Birinchisini qo'shing!",
  faol: "Faol vazifa yo'q.",
  bajarilgan: "Bajarilgan vazifa hali yo'q.",
};
const filtrlar = document.getElementById("filtrlar");
const boshXabar = document.getElementById("bosh-xabar");

function mosmi(vazifa, filtr) {
  if (filtr === "faol") {
    return !vazifa.bajarildi;
  }
  if (filtr === "bajarilgan") {
    return vazifa.bajarildi;
  }
  return true;
}

function korinadiganlar() {
  const natija = [];
  for (const vazifa of holat.vazifalar) {
    if (mosmi(vazifa, holat.filtr)) {
      natija.push(vazifa);
    }
  }
  return natija;
}

function filtrniTanla(filtr) {
  holat.filtr = filtr;
  holat.tahrirId = null;
  render();
}

filtrlar.addEventListener("click", (event) => {
  const tugma = event.target.closest("[data-filtr]");
  if (tugma === null) {
    return;
  }
  elonQil("");
  filtrniTanla(tugma.dataset.filtr);
});

render() endi korinadiganlar() ni aylanadi, replaceChildren dan keyin esa:

js
boshXabar.hidden = royxat.children.length > 0;
matnniYoz(boshXabar, boshMatnlar[holat.filtr]);
for (const tugma of filtrlar.querySelectorAll("[data-filtr]")) {
  const tanlangan = tugma.dataset.filtr === holat.filtr;
  tugma.setAttribute("aria-pressed", String(tanlangan));
}

vazifaQosh da render() dan oldin:

js
if (holat.filtr === "bajarilgan") {
  holat.filtr = "hammasi";
}

Uch eslatma:

  • Hisoblagich filtrga qaramaydi. "Faol" filtrida ham, "Bajarilgan" da ham "N ta vazifa qoldi" — butun ro'yxat bo'yicha. U holat.vazifalar dan sanaladi, ekrandagi li lardan emas (qoidaning birinchisi: "DOM'dan so'ramang, holatdan so'rang").
  • Filtr vazifani o'chirmaydi. korinadiganlar() yangi massiv yig'adi, holat.vazifalar ga tegmaydi. Filtrlangan ro'yxat — hosila ma'lumot.
  • Belgilangan vazifa "Faol" dan chiqib ketadi. Shuning uchun 4-bosqichdagi fokusla ga zaxira kerak edi: o'z katagi yo'q bo'lsa, fokus qo'shnisiga o'tadi.

Mana loyihaning to'liq ishlaydigan versiyasi. Oxirgi uch qator — sinov qatorlari, faylga ko'chirmang:

html
<style>
  :root {
    --rang-matn: #1f2a24;
    --rang-fon: #f7faf8;
    --rang-asosiy: #1f6f4a;
    --rang-chegara: #c9d6cf;
    --rang-ikkinchi: #5b6660;
    --rang-xato: #b3261e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--rang-matn);
    background: var(--rang-fon);
  }
  main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
  h1 { margin-top: 0; color: var(--rang-asosiy); }
  h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
  button, input { font: inherit; }
  button {
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rang-chegara);
    border-radius: 0.5rem;
    background: #fff;
    color: var(--rang-matn);
  }
  button[aria-pressed="true"] {
    border-color: var(--rang-asosiy);
    background: var(--rang-asosiy);
    color: #fff;
  }
  :focus-visible {
    outline: 3px solid var(--rang-asosiy);
    outline-offset: 2px;
  }
  .qator { display: flex; gap: 0.5rem; }
  #yangi-matn, #tahrir-maydon {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.5rem;
    border: 2px solid var(--rang-ikkinchi);
    border-radius: 0.5rem;
  }
  [aria-invalid="true"] { border-color: var(--rang-xato); }
  .xato {
    min-height: 1.5em;
    margin: 0.25rem 0 0;
    color: var(--rang-xato);
  }
  #filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  #royxat { margin: 0; padding: 0; list-style: none; }
  #royxat li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rang-chegara);
  }
  #royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
  #royxat [type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bajarildi label {
    color: var(--rang-ikkinchi);
    text-decoration: line-through;
  }
  #hisob { font-weight: bold; }
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="elon" role="status"></p>
  <p><small>v1: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi.</small></p>
</main>
<script>
  // vazifalar.js — vazifalar ro'yxati v1 (09-qism yakuniy loyihasi)

  // 1. Holat — yagona haqiqat manbai
  const holat = {
    vazifalar: [
      { id: 1, matn: "Non olish", bajarildi: false },
      { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
      { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
    ],
    keyingiId: 4,
    filtr: "hammasi",
    tahrirId: null,
  };

  const boshMatnlar = {
    hammasi: "Hozircha vazifa yo'q. Birinchisini qo'shing!",
    faol: "Faol vazifa yo'q.",
    bajarilgan: "Bajarilgan vazifa hali yo'q.",
  };

  // 2. Sahifa elementlari
  const forma = document.getElementById("yangi-forma");
  const maydon = document.getElementById("yangi-matn");
  const maydonXato = document.getElementById("yangi-matn-xato");
  const filtrlar = document.getElementById("filtrlar");
  const sarlavha = document.getElementById("royxat-sarlavha");
  const royxat = document.getElementById("royxat");
  const boshXabar = document.getElementById("bosh-xabar");
  const hisob = document.getElementById("hisob");
  const elon = document.getElementById("elon");

  // 3. Hisoblovchi funksiyalar: faqat qiymat qaytaradi
  function xatoniTop(matn, vazifalar, ozId) {
    if (matn === "") {
      return "Vazifa matnini yozing.";
    }
    const kichik = matn.toLowerCase();
    const takror = vazifalar.some(
      (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
    );
    if (takror) {
      return "Bu vazifa ro'yxatda allaqachon bor.";
    }
    return null;
  }

  function mosmi(vazifa, filtr) {
    if (filtr === "faol") {
      return !vazifa.bajarildi;
    }
    if (filtr === "bajarilgan") {
      return vazifa.bajarildi;
    }
    return true;
  }

  function korinadiganlar() {
    const natija = [];
    for (const vazifa of holat.vazifalar) {
      if (mosmi(vazifa, holat.filtr)) {
        natija.push(vazifa);
      }
    }
    return natija;
  }

  function faollarSoni() {
    let soni = 0;
    for (const vazifa of holat.vazifalar) {
      if (!vazifa.bajarildi) {
        soni++;
      }
    }
    return soni;
  }

  function vazifaniTop(id) {
    return holat.vazifalar.find((v) => v.id === id);
  }

  // 4. Chizish: holatdan ekranga
  function tugmaYasa(amal, matn, nomi) {
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.dataset.amal = amal;
    tugma.textContent = matn;
    tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
    return tugma;
  }

  function oddiyQator(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    li.classList.toggle("bajarildi", vazifa.bajarildi);
    const katak = document.createElement("input");
    katak.type = "checkbox";
    katak.id = `vazifa-${vazifa.id}`;
    katak.dataset.amal = "almashtir";
    katak.checked = vazifa.bajarildi;
    const yorliq = document.createElement("label");
    yorliq.htmlFor = katak.id;
    yorliq.textContent = vazifa.matn;
    li.append(
      katak,
      yorliq,
      tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
      tugmaYasa("ochir", "O'chirish", vazifa.matn),
    );
    return li;
  }

  function tahrirQatori(vazifa) {
    const li = document.createElement("li");
    li.dataset.id = vazifa.id;
    const kiritma = document.createElement("input");
    kiritma.id = "tahrir-maydon";
    kiritma.value = vazifa.matn;
    kiritma.maxLength = 100;
    kiritma.setAttribute("aria-label", "Vazifa matni");
    li.append(
      kiritma,
      tugmaYasa("saqla", "Saqlash", vazifa.matn),
      tugmaYasa("bekor", "Bekor qilish", vazifa.matn),
    );
    return li;
  }

  function matnniYoz(element, matn) {
    if (element.textContent !== matn) {
      element.textContent = matn;
    }
  }

  function render() {
    const quti = document.createDocumentFragment();
    for (const vazifa of korinadiganlar()) {
      if (vazifa.id === holat.tahrirId) {
        quti.append(tahrirQatori(vazifa));
      } else {
        quti.append(oddiyQator(vazifa));
      }
    }
    royxat.replaceChildren(quti);

    boshXabar.hidden = royxat.children.length > 0;
    matnniYoz(boshXabar, boshMatnlar[holat.filtr]);
    for (const tugma of filtrlar.querySelectorAll("[data-filtr]")) {
      const tanlangan = tugma.dataset.filtr === holat.filtr;
      tugma.setAttribute("aria-pressed", String(tanlangan));
    }
    matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
  }

  // 5. Holatni o'zgartiruvchi funksiyalar: holat → render()
  function vazifaQosh(matn) {
    holat.vazifalar.push({
      id: holat.keyingiId,
      matn,
      bajarildi: false,
    });
    holat.keyingiId++;
    if (holat.filtr === "bajarilgan") {
      holat.filtr = "hammasi";
    }
    render();
  }

  function vazifaniAlmashtir(id) {
    const vazifa = vazifaniTop(id);
    vazifa.bajarildi = !vazifa.bajarildi;
    render();
  }

  function vazifaniOchir(id) {
    const orin = holat.vazifalar.findIndex((v) => v.id === id);
    holat.vazifalar.splice(orin, 1);
    render();
  }

  function tahrirniBoshla(id) {
    holat.tahrirId = id;
    render();
  }

  function matnniAlmashtir(id, matn) {
    vazifaniTop(id).matn = matn;
    holat.tahrirId = null;
    render();
  }

  function tahrirniYop() {
    holat.tahrirId = null;
    render();
  }

  function filtrniTanla(filtr) {
    holat.filtr = filtr;
    holat.tahrirId = null;
    render();
  }

  // 6. Fokus va e'lon: render'dan keyin
  function fokusla(id, amal, zaxiraId) {
    const top = (i) =>
      royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
    const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
    nishon.focus();
  }

  function qoshniId(id) {
    const qator = royxat.querySelector(`[data-id="${id}"]`);
    const qoshni =
      qator.nextElementSibling ?? qator.previousElementSibling;
    return qoshni === null ? null : Number(qoshni.dataset.id);
  }

  function elonQil(matn) {
    elon.textContent = matn;
  }

  function xatoniKorsat(xabar) {
    maydonXato.textContent = xabar;
    maydon.setAttribute("aria-invalid", "true");
    maydon.setAttribute("aria-describedby", maydonXato.id);
  }

  function xatoniTozala() {
    maydonXato.textContent = "";
    maydon.removeAttribute("aria-invalid");
    maydon.removeAttribute("aria-describedby");
  }

  function tahrirgaOt() {
    const kiritma = document.getElementById("tahrir-maydon");
    kiritma.focus();
    kiritma.select();
  }

  function tahrirniSaqla() {
    const kiritma = document.getElementById("tahrir-maydon");
    const id = holat.tahrirId;
    const matn = kiritma.value.trim();
    const xato = xatoniTop(matn, holat.vazifalar, id);
    if (xato !== null) {
      kiritma.setAttribute("aria-invalid", "true");
      kiritma.focus();
      elonQil(xato);
      return;
    }
    matnniAlmashtir(id, matn);
    fokusla(id, "tahrirla", null);
    elonQil("O'zgarish saqlandi.");
  }

  function tahrirniBekorQil() {
    const id = holat.tahrirId;
    tahrirniYop();
    fokusla(id, "tahrirla", null);
  }

  // 7. Hodisalar: har joyda bitta tinglovchi
  forma.addEventListener("submit", (event) => {
    event.preventDefault();
    elonQil("");
    const matn = maydon.value.trim();
    const xato = xatoniTop(matn, holat.vazifalar, null);
    if (xato !== null) {
      xatoniKorsat(xato);
      maydon.focus();
      return;
    }
    xatoniTozala();
    vazifaQosh(matn);
    forma.reset();
    elonQil(`«${matn}» qo'shildi.`);
  });

  maydon.addEventListener("input", () => {
    if (maydon.getAttribute("aria-invalid") !== "true") {
      return;
    }
    const matn = maydon.value.trim();
    if (xatoniTop(matn, holat.vazifalar, null) === null) {
      xatoniTozala();
    }
  });

  royxat.addEventListener("click", (event) => {
    const nishon = event.target.closest("[data-amal]");
    if (nishon === null || !royxat.contains(nishon)) {
      return;
    }
    elonQil("");
    const amal = nishon.dataset.amal;
    const id = Number(nishon.closest("li").dataset.id);
    const zaxira = qoshniId(id);
    if (amal !== "saqla" && amal !== "bekor") {
      holat.tahrirId = null;
    }
    switch (amal) {
      case "almashtir":
        vazifaniAlmashtir(id);
        fokusla(id, "almashtir", zaxira);
        break;
      case "ochir":
        elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
        vazifaniOchir(id);
        fokusla(zaxira, "ochir", null);
        break;
      case "tahrirla":
        tahrirniBoshla(id);
        tahrirgaOt();
        break;
      case "saqla":
        tahrirniSaqla();
        break;
      case "bekor":
        tahrirniBekorQil();
        break;
    }
  });

  royxat.addEventListener("keydown", (event) => {
    if (event.target.id !== "tahrir-maydon" || event.isComposing) {
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      tahrirniSaqla();
    } else if (event.key === "Escape") {
      tahrirniBekorQil();
    }
  });

  filtrlar.addEventListener("click", (event) => {
    const tugma = event.target.closest("[data-filtr]");
    if (tugma === null) {
      return;
    }
    elonQil("");
    filtrniTanla(tugma.dataset.filtr);
  });

  render();

  // Sinov qatorlari — faylga ko'chirmang
  console.log(royxat.children);
  console.log(hisob.textContent);
</script>

Konsolda:

text
HTMLCollection(3) [li, li.bajarildi, li]
2 ta vazifa qoldi

Sinab ko'ring:

  • «Faol» — ikki vazifa qoldi, «Faol» tugmasi to'q yashil (aria-pressed="true").
  • "Faol" da turib "Non olish" katagini Space bilan belgilang — vazifa ro'yxatdan chiqadi, fokus "Onamga qo'ng'iroq qilish" katagiga o'tadi, "1 ta vazifa qoldi".
  • Uni ham belgilang — ro'yxat bo'sh, "Faol vazifa yo'q.", fokus "Ro'yxat" sarlavhasida.
  • «Bajarilgan» — uchala vazifa chizilgan. Shu filtrda yangi vazifa qo'shing — filtr o'zi «Hammasi» ga o'tadi.
To'liq fayllar: index.html, vazifalar.css, vazifalar.js (nusxa olish uchun)

index.html — «1-bosqich» dagi fayl. assets/css/vazifalar.css:

css
/* vazifalar.css — vazifalar ro'yxati v1 */
:root {
  --rang-matn: #1f2a24;
  --rang-fon: #f7faf8;
  --rang-asosiy: #1f6f4a;
  --rang-chegara: #c9d6cf;
  --rang-ikkinchi: #5b6660;
  --rang-xato: #b3261e;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font: 1rem/1.5 system-ui, sans-serif;
  color: var(--rang-matn);
  background: var(--rang-fon);
}
main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
h1 { margin-top: 0; color: var(--rang-asosiy); }
h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
button, input { font: inherit; }
button {
  min-height: 2.75rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--rang-chegara);
  border-radius: 0.5rem;
  background: #fff;
  color: var(--rang-matn);
}
button[aria-pressed="true"] {
  border-color: var(--rang-asosiy);
  background: var(--rang-asosiy);
  color: #fff;
}
:focus-visible {
  outline: 3px solid var(--rang-asosiy);
  outline-offset: 2px;
}
.qator { display: flex; gap: 0.5rem; }
#yangi-matn, #tahrir-maydon {
  flex: 1;
  min-width: 0;
  min-height: 2.75rem;
  padding: 0.5rem;
  border: 2px solid var(--rang-ikkinchi);
  border-radius: 0.5rem;
}
[aria-invalid="true"] { border-color: var(--rang-xato); }
.xato {
  min-height: 1.5em;
  margin: 0.25rem 0 0;
  color: var(--rang-xato);
}
#filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
#royxat { margin: 0; padding: 0; list-style: none; }
#royxat li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--rang-chegara);
}
#royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
#royxat [type="checkbox"] {
  width: 1.25rem;
  height: 1.25rem;
}
.bajarildi label {
  color: var(--rang-ikkinchi);
  text-decoration: line-through;
}
#hisob { font-weight: bold; }

assets/js/vazifalar.js:

js
// vazifalar.js — vazifalar ro'yxati v1 (09-qism yakuniy loyihasi)

// 1. Holat — yagona haqiqat manbai
const holat = {
  vazifalar: [
    { id: 1, matn: "Non olish", bajarildi: false },
    { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
    { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
  ],
  keyingiId: 4,
  filtr: "hammasi",
  tahrirId: null,
};

const boshMatnlar = {
  hammasi: "Hozircha vazifa yo'q. Birinchisini qo'shing!",
  faol: "Faol vazifa yo'q.",
  bajarilgan: "Bajarilgan vazifa hali yo'q.",
};

// 2. Sahifa elementlari
const forma = document.getElementById("yangi-forma");
const maydon = document.getElementById("yangi-matn");
const maydonXato = document.getElementById("yangi-matn-xato");
const filtrlar = document.getElementById("filtrlar");
const sarlavha = document.getElementById("royxat-sarlavha");
const royxat = document.getElementById("royxat");
const boshXabar = document.getElementById("bosh-xabar");
const hisob = document.getElementById("hisob");
const elon = document.getElementById("elon");

// 3. Hisoblovchi funksiyalar: faqat qiymat qaytaradi
function xatoniTop(matn, vazifalar, ozId) {
  if (matn === "") {
    return "Vazifa matnini yozing.";
  }
  const kichik = matn.toLowerCase();
  const takror = vazifalar.some(
    (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
  );
  if (takror) {
    return "Bu vazifa ro'yxatda allaqachon bor.";
  }
  return null;
}

function mosmi(vazifa, filtr) {
  if (filtr === "faol") {
    return !vazifa.bajarildi;
  }
  if (filtr === "bajarilgan") {
    return vazifa.bajarildi;
  }
  return true;
}

function korinadiganlar() {
  const natija = [];
  for (const vazifa of holat.vazifalar) {
    if (mosmi(vazifa, holat.filtr)) {
      natija.push(vazifa);
    }
  }
  return natija;
}

function faollarSoni() {
  let soni = 0;
  for (const vazifa of holat.vazifalar) {
    if (!vazifa.bajarildi) {
      soni++;
    }
  }
  return soni;
}

function vazifaniTop(id) {
  return holat.vazifalar.find((v) => v.id === id);
}

// 4. Chizish: holatdan ekranga
function tugmaYasa(amal, matn, nomi) {
  const tugma = document.createElement("button");
  tugma.type = "button";
  tugma.dataset.amal = amal;
  tugma.textContent = matn;
  tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
  return tugma;
}

function oddiyQator(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  li.classList.toggle("bajarildi", vazifa.bajarildi);
  const katak = document.createElement("input");
  katak.type = "checkbox";
  katak.id = `vazifa-${vazifa.id}`;
  katak.dataset.amal = "almashtir";
  katak.checked = vazifa.bajarildi;
  const yorliq = document.createElement("label");
  yorliq.htmlFor = katak.id;
  yorliq.textContent = vazifa.matn;
  li.append(
    katak,
    yorliq,
    tugmaYasa("tahrirla", "Tahrirlash", vazifa.matn),
    tugmaYasa("ochir", "O'chirish", vazifa.matn),
  );
  return li;
}

function tahrirQatori(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  const kiritma = document.createElement("input");
  kiritma.id = "tahrir-maydon";
  kiritma.value = vazifa.matn;
  kiritma.maxLength = 100;
  kiritma.setAttribute("aria-label", "Vazifa matni");
  li.append(
    kiritma,
    tugmaYasa("saqla", "Saqlash", vazifa.matn),
    tugmaYasa("bekor", "Bekor qilish", vazifa.matn),
  );
  return li;
}

function matnniYoz(element, matn) {
  if (element.textContent !== matn) {
    element.textContent = matn;
  }
}

function render() {
  const quti = document.createDocumentFragment();
  for (const vazifa of korinadiganlar()) {
    if (vazifa.id === holat.tahrirId) {
      quti.append(tahrirQatori(vazifa));
    } else {
      quti.append(oddiyQator(vazifa));
    }
  }
  royxat.replaceChildren(quti);

  boshXabar.hidden = royxat.children.length > 0;
  matnniYoz(boshXabar, boshMatnlar[holat.filtr]);
  for (const tugma of filtrlar.querySelectorAll("[data-filtr]")) {
    const tanlangan = tugma.dataset.filtr === holat.filtr;
    tugma.setAttribute("aria-pressed", String(tanlangan));
  }
  matnniYoz(hisob, `${faollarSoni()} ta vazifa qoldi`);
}

// 5. Holatni o'zgartiruvchi funksiyalar: holat → render()
function vazifaQosh(matn) {
  holat.vazifalar.push({
    id: holat.keyingiId,
    matn,
    bajarildi: false,
  });
  holat.keyingiId++;
  if (holat.filtr === "bajarilgan") {
    holat.filtr = "hammasi";
  }
  render();
}

function vazifaniAlmashtir(id) {
  const vazifa = vazifaniTop(id);
  vazifa.bajarildi = !vazifa.bajarildi;
  render();
}

function vazifaniOchir(id) {
  const orin = holat.vazifalar.findIndex((v) => v.id === id);
  holat.vazifalar.splice(orin, 1);
  render();
}

function tahrirniBoshla(id) {
  holat.tahrirId = id;
  render();
}

function matnniAlmashtir(id, matn) {
  vazifaniTop(id).matn = matn;
  holat.tahrirId = null;
  render();
}

function tahrirniYop() {
  holat.tahrirId = null;
  render();
}

function filtrniTanla(filtr) {
  holat.filtr = filtr;
  holat.tahrirId = null;
  render();
}

// 6. Fokus va e'lon: render'dan keyin
function fokusla(id, amal, zaxiraId) {
  const top = (i) =>
    royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
  const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
  nishon.focus();
}

function qoshniId(id) {
  const qator = royxat.querySelector(`[data-id="${id}"]`);
  const qoshni =
    qator.nextElementSibling ?? qator.previousElementSibling;
  return qoshni === null ? null : Number(qoshni.dataset.id);
}

function elonQil(matn) {
  elon.textContent = matn;
}

function xatoniKorsat(xabar) {
  maydonXato.textContent = xabar;
  maydon.setAttribute("aria-invalid", "true");
  maydon.setAttribute("aria-describedby", maydonXato.id);
}

function xatoniTozala() {
  maydonXato.textContent = "";
  maydon.removeAttribute("aria-invalid");
  maydon.removeAttribute("aria-describedby");
}

function tahrirgaOt() {
  const kiritma = document.getElementById("tahrir-maydon");
  kiritma.focus();
  kiritma.select();
}

function tahrirniSaqla() {
  const kiritma = document.getElementById("tahrir-maydon");
  const id = holat.tahrirId;
  const matn = kiritma.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, id);
  if (xato !== null) {
    kiritma.setAttribute("aria-invalid", "true");
    kiritma.focus();
    elonQil(xato);
    return;
  }
  matnniAlmashtir(id, matn);
  fokusla(id, "tahrirla", null);
  elonQil("O'zgarish saqlandi.");
}

function tahrirniBekorQil() {
  const id = holat.tahrirId;
  tahrirniYop();
  fokusla(id, "tahrirla", null);
}

// 7. Hodisalar: har joyda bitta tinglovchi
forma.addEventListener("submit", (event) => {
  event.preventDefault();
  elonQil("");
  const matn = maydon.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, null);
  if (xato !== null) {
    xatoniKorsat(xato);
    maydon.focus();
    return;
  }
  xatoniTozala();
  vazifaQosh(matn);
  forma.reset();
  elonQil(`«${matn}» qo'shildi.`);
});

maydon.addEventListener("input", () => {
  if (maydon.getAttribute("aria-invalid") !== "true") {
    return;
  }
  const matn = maydon.value.trim();
  if (xatoniTop(matn, holat.vazifalar, null) === null) {
    xatoniTozala();
  }
});

royxat.addEventListener("click", (event) => {
  const nishon = event.target.closest("[data-amal]");
  if (nishon === null || !royxat.contains(nishon)) {
    return;
  }
  elonQil("");
  const amal = nishon.dataset.amal;
  const id = Number(nishon.closest("li").dataset.id);
  const zaxira = qoshniId(id);
  if (amal !== "saqla" && amal !== "bekor") {
    holat.tahrirId = null;
  }
  switch (amal) {
    case "almashtir":
      vazifaniAlmashtir(id);
      fokusla(id, "almashtir", zaxira);
      break;
    case "ochir":
      elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
      vazifaniOchir(id);
      fokusla(zaxira, "ochir", null);
      break;
    case "tahrirla":
      tahrirniBoshla(id);
      tahrirgaOt();
      break;
    case "saqla":
      tahrirniSaqla();
      break;
    case "bekor":
      tahrirniBekorQil();
      break;
  }
});

royxat.addEventListener("keydown", (event) => {
  if (event.target.id !== "tahrir-maydon" || event.isComposing) {
    return;
  }
  if (event.key === "Enter") {
    event.preventDefault();
    tahrirniSaqla();
  } else if (event.key === "Escape") {
    tahrirniBekorQil();
  }
});

filtrlar.addEventListener("click", (event) => {
  const tugma = event.target.closest("[data-filtr]");
  if (tugma === null) {
    return;
  }
  elonQil("");
  filtrniTanla(tugma.dataset.filtr);
});

render();

Commit: Filtr: filtr va bo'sh holatni qo'sh.

10. 7-bosqich: sifat tekshiruvi

Ilova ishlaydi. Endi uni hujumchi, klaviatura foydalanuvchisi va "ertangi o'zingiz" ko'zi bilan tekshiramiz.

10.1 XSS sinovi

Yakuniy oynadagi maydonga aynan shuni yozib, Enter bosing:

text
<img src=x onerror="console.log('XSS')">

Ro'yxatda shu satr matn bo'lib turadi: rasm yo'q, konsol bo'sh. Agar yorliq.innerHTML = vazifa.matn yozilganida, brauzer img yaratardi va onerror ichidagi kod ishlardi (Kontentni o'zgartirish va XSS xavfi). console.log o'rniga hujumchi istalgan kodni qo'yadi.

10.2 Klaviatura sinovi

Sichqonchani chetga suring va faqat klaviatura bilan bajaring:

  1. Tab — maydon, «Qo'shish», uchta filtr tugmasi, keyin har qatorning katagi va tugmalari. Fokus halqasi doim ko'rinadimi?
  2. Yangi vazifa qo'shing, uni Space bilan belgilang, «Tahrirlash» → yangi matn → Enter, keyin «Tahrirlash» → Esc.
  3. Oxirgi vazifani o'chiring — fokus sarlavhaga tushadimi, body ga emasmi?

10.3 Accessibility yorlig'i

DevTools → Elements → biror «O'chirish» tugmasi → o'ngdagi Accessibility yorlig'i. Name — "O'chirish: Non olish". Filtr tugmasida Pressed — true yoki false. Hisoblagichda Live region — polite.

10.4 Debug: Number unutilsa

4-bosqichdagi Number(...) ni unutish — bu loyihadagi eng keng tarqalgan xato. Uni ataylab qilamiz va Debugging metodologiyasi darsidagi kabi minimal takrorlash bilan ko'ramiz: ikki vazifa, bitta tinglovchi, ortiqcha hech narsa. Biror katakni bosing:

html
<ul id="royxat"></ul>
<p id="hisob"></p>
<script>
  const holat = {
    vazifalar: [
      { id: 1, matn: "Non olish", bajarildi: false },
      { id: 2, matn: "Kitob o'qish", bajarildi: false },
    ],
  };
  const royxat = document.getElementById("royxat");
  const hisob = document.getElementById("hisob");

  function render() {
    royxat.replaceChildren();
    let qoldi = 0;
    for (const vazifa of holat.vazifalar) {
      const li = document.createElement("li");
      li.dataset.id = vazifa.id;
      const yorliq = document.createElement("label");
      const katak = document.createElement("input");
      katak.type = "checkbox";
      katak.dataset.amal = "almashtir";
      katak.checked = vazifa.bajarildi;
      yorliq.append(katak, ` ${vazifa.matn}`);
      li.append(yorliq);
      royxat.append(li);
      if (!vazifa.bajarildi) {
        qoldi++;
      }
    }
    hisob.textContent = `${qoldi} ta vazifa qoldi`;
  }

  function vazifaniAlmashtir(id) {
    const vazifa = holat.vazifalar.find((v) => v.id === id);
    vazifa.bajarildi = !vazifa.bajarildi;
    render();
  }

  royxat.addEventListener("click", (event) => {
    const nishon = event.target.closest("[data-amal]");
    if (nishon === null) {
      return;
    }
    const id = nishon.closest("li").dataset.id;
    vazifaniAlmashtir(id);
  });

  render();
</script>

Katakni bosganingizda (sahifa ochilganda emas) konsolda shu qizil qator chiqadi:

text
Uncaught TypeError: Cannot read properties of undefined (reading 'bajarildi')

Tarjimasi: "undefined ning xususiyatini o'qib bo'lmaydi (bajarildi ni o'qishda)". Katak belgilandi, lekin hisob hamon "2 ta" — xato holat o'zgarishidan oldin yiqildi.

To'liq faylda, Live Server'da xuddi shu xato to'liq iz bilan chiqadi. Biz uni Chrome 154 da haqiqiy loyihada qo'zg'atdik:

text
Uncaught TypeError: Cannot read properties of undefined (reading 'bajarildi')
    at vazifaniAlmashtir (vazifalar.js:170:30)
    at HTMLUListElement.<anonymous> (vazifalar.js:302:7)

Stack trace darsidagidek tepadan pastga o'qiymiz. Birinchi at — yiqilgan joy: vazifaniAlmashtir, 170-qator, vazifa — undefined. Uni 302-qatordan ul ning nomsiz tinglovchisi chaqirgan.

Aybdor joy esa bir qadam orqada: find hech narsa topmagan. Gipoteza: "id noto'g'ri turda". Tekshiruv — "Tahrirlab ko'rish" bilan tinglovchidagi id qatoridan keyin bitta log qo'shing:

js
console.log(typeof id, id); // DEBUG

Katakni bosing — konsolda string 1, keyin o'sha qizil xato. Gipoteza tasdiqlandi: 1 === "1" — false. Tuzatish — bitta o'zgarish: Number(nishon.closest("li").dataset.id). Shundan keyin katak bosilganda hisob "1 ta vazifa qoldi" bo'ladi. Log qatorini o'chiring.

11. 8-bosqich: GitHub va Pages

11.1 Tarix

Har bosqich o'z branch'ida qilingan va main ga qo'shilgan bo'lsa, git log --oneline taxminan shunday ko'rinadi (SHA'lar sizda boshqacha):

Branch Commit xabari
main Vazifalar: sahifa skeletini yarat
feature/render Render: vazifalarni holatdan chiz
feature/qoshish Forma: vazifa qo'shishni ula
feature/belgilash-ochirish Ro'yxat: belgilash va o'chirishni delegation bilan ula
feature/tahrirlash Tahrir: Enter va Esc bilan tahrirlash rejimini qo'sh
feature/filtr Filtr: filtr va bo'sh holatni qo'sh

Har birini git merge bilan qo'shgach, branch'ni o'chiring: git branch -d feature/filtr (Merge). Oxirida — README.md:

markdown
# Vazifalar

Vanilla JavaScript'da yozilgan vazifalar ro'yxati (v1).
Qo'shish, belgilash, tahrirlash, o'chirish va filtr;
klaviatura va ekran o'quvchi bilan ishlaydi.

Sayt: https://login.github.io/vazifalar/

v1 ma'lumotni saqlamaydi — sahifa yangilansa, ro'yxat tozalanadi.
bash
git add README.md
git commit -m "Docs: README qo'sh"

11.2 GitHub'ga yuborish

GitHub CLI bilan bitta buyruq — repo yaratadi, origin ni ulaydi va yuboradi:

bash
gh repo create vazifalar --public --source=. --push

gh sozlanmagan bo'lsa — brauzerda bo'sh vazifalar repo yarating, keyin git remote add origin va git push -u origin main (Remote, push va pull).

11.3 Pages

Bu loyihada build yo'q, package.json yo'q, fayllar repo ildizida va ichida sir yo'q. Shuning uchun eng sodda manba yetadi:

  1. Repo → Settings → Pages → Build and deployment.
  2. Source — Deploy from a branch, Branch — main, papka — / (root) → Save.
  3. Bir-ikki daqiqadan keyin: https://login.github.io/vazifalar/.

Bu — loyiha sayti: u domen ildizida emas, /vazifalar/ papkasida yashaydi. Shuning uchun index.html dagi yo'llar nisbiy (assets/...). /assets/... deb yozilganida, brauzer CSS va JS'ni login.github.io/assets/ dan so'rardi va sayt bezaksiz, jonsiz ochilardi (GitHub Pages'ga avtomatik deploy darsidagi yo'l tuzog'i). Portfolio'dagidek GitHub Actions yo'li ham ishlaydi — u loyiha Vite bilan yig'iladigan bo'lganda (16-qism) kerak bo'ladi.

Deploy'dan keyin telefoningizda saytni oching va «Klaviatura sinovi» dagi qadamlarni yana bajaring.

12. Baholash ro'yxati

O'zingiz baholang, yaxshisi — kursdoshingiz.

Majburiy shartlar. Bittasi bajarilmasa, loyiha qabul qilinmaydi:

  • Sayt https://login.github.io/vazifalar/ da ochiladi, CSS va JS yuklangan.
  • Ishlatish paytida konsolda qizil xato yo'q.
  • XSS sinovi: <img src=x onerror=...> matn bo'lib chiqadi.
  • Hamma amal faqat klaviatura bilan bajariladi.

Ballar (jami 100, o'tish — 80):

Bo'lim Nima tekshiriladi Ball
Tuzilma Fayllar assets/ da, nisbiy yo'llar, defer 10
Holat → render Ma'lumot faqat holatda, hosila saqlanmaydi, render() holatni o'zgartirmaydi 15
Qo'shish submit + preventDefault, trim, bo'sh va takror rad etiladi, aria-invalid 15
Delegation ul ga bitta tinglovchi, closest + null tekshiruvi, data-amal, Number 10
Tahrirlash Enter/Esc va tugmalar, bekor qilish holatga tegmaydi 15
Filtr va hisob for...of, aria-pressed, aria-live, bo'sh holat matni 10
Fokus Belgilash, o'chirish va tahrirdan keyin fokus mantiqiy joyda 10
Git Branch'lar, kelishuvdagi commit xabarlari, README 10
Kod sifati Nomlar fe'l yoki natija nomi, // DEBUG qolmagan 5

Har bo'limda: hammasi bajarilgan — to'liq ball, bitta kamchilik — yarmi, ko'p — 0. 90–100 — portfolio'ga qo'shing (4-mashq). 80–89 — kamchiliklarni tuzating. 80 dan kam — ball yo'qolgan bo'limning tayanch darsiga qayting.

13. Ko'p uchraydigan xatolar

13.1 dataset dan kelgan satr

find((v) => v.id === id) — undefined, keyin Cannot read properties of undefined. Atribut doim satr (dataset). Tuzatish: Number(...) — o'qish joyining o'zida.

13.2 Fokus body ga tushadi

render() dan keyin hech narsa qilinmasa, fokusdagi tugma o'chadi va document.activeElement — BODY. Sichqoncha foydalanuvchisi sezmaydi, Bobur esa sahifa boshidan qayta yuradi. Tuzatish: har amaldan keyin fokusla.

13.3 Tinglovchi qayta chiziladigan elementda

Tinglovchini li yoki tugmaga render() ichida qo'yish — har chizishda yangi tinglovchilar. Tahrir maydoniga keydown ni to'g'ridan-to'g'ri qo'yish ham shunday. Tuzatish: tinglovchi o'zgarmaydigan ul da.

13.4 Matn innerHTML bilan

Shablon satr bilan li.innerHTML = `<label>${vazifa.matn}</label>` — qisqa ko'rinadi, lekin XSS eshigi. Tuzatish: createElement + textContent.

13.5 / bilan boshlangan yo'l

Lokal Live Server'da hammasi ishlaydi, Pages'da sahifa bezaksiz va konsolda 404. Tuzatish: assets/css/... — boshida / siz.

14. Hujumchi nigohi

  • Begona matn. Vazifa matni — foydalanuvchi kiritgan ma'lumot. Bugun uni faqat egasi yozadi, lekin 11-qismda u saqlanadi, keyinroq serverdan keladi. Biz uni hamma joyda textContent va setAttribute bilan qo'ydik — hujumchi yozgan HTML hech qayerda kod bo'lib ishlamaydi.
  • Uzun matn. maxlength="100" ni DevTools'da o'chirish mumkin. Ilova buzilmaydi: overflow-wrap: anywhere uzun so'zni ham sindiradi. Server paydo bo'lganda cheklov u yerda qayta tekshiriladi.
  • Ochiq repo. Pages'dagi va GitHub'dagi har fayl hammaga ochiq. Namuna vazifalarga shaxsiy ma'lumot yozmang.

15. Mashqlar

1-mashq (oson): "Hammasi bajarildi"

Hamma vazifa bajarilganda hisoblagichda "0 ta vazifa qoldi" o'rniga Hamma vazifa bajarildi! chiqsin. Qaysi funksiyani o'zgartirasiz?

Ishora: bu hosila matn — uni faqat render() yozadi.

Yechim

render() dagi oxirgi qator o'rniga:

js
const qoldi = faollarSoni();
if (qoldi === 0) {
  matnniYoz(hisob, "Hamma vazifa bajarildi!");
} else {
  matnniYoz(hisob, `${qoldi} ta vazifa qoldi`);
}

Yakuniy oynada "Non olish" va "Onamga qo'ng'iroq qilish" ni belgilang — hisoblagichda "Hamma vazifa bajarildi!". Boshqa hech qaysi funksiya o'zgarmadi: hisoblagich hosila ma'lumot.

2-mashq (o'rta): bajarilganlarni tozalash

Hisoblagich ostiga <button type="button" id="tozala">Bajarilganlarni tozalash</button> qo'ying. Bosilganda bajarilgan hamma vazifa o'chsin, #elon da "1 ta bajarilgan vazifa o'chirildi." chiqsin. Bajarilgan vazifa yo'q bo'lsa, tugma yashirinsin (hidden).

Ishora: filter siz — for...of bilan yangi massiv yig'ib, holat.vazifalar ga bering (Holatdan UI chizish 3-mashqi). Ehtiyot bo'ling: tugma bosilgach o'zi yashirinadi — fokus body ga tushmasin.

Yechim
js
const tozalaTugma = document.getElementById("tozala");

function bajarilganlarniTozala() {
  const qolganlar = [];
  for (const vazifa of holat.vazifalar) {
    if (!vazifa.bajarildi) {
      qolganlar.push(vazifa);
    }
  }
  const soni = holat.vazifalar.length - qolganlar.length;
  holat.vazifalar = qolganlar;
  holat.tahrirId = null;
  render();
  return soni;
}

tozalaTugma.addEventListener("click", () => {
  const soni = bajarilganlarniTozala();
  sarlavha.focus();
  elonQil(`${soni} ta bajarilgan vazifa o'chirildi.`);
});

render() oxiriga: tozalaTugma.hidden = faollarSoni() === holat.vazifalar.length; — faollar soni hammasiga teng bo'lsa, bajarilgani yo'q.

Bosganda "DOM darsini takrorlash" o'chadi, pastda "1 ta bajarilgan vazifa o'chirildi.", tugma yashirinadi, fokus "Ro'yxat" sarlavhasida. sarlavha.focus() siz fokus yashiringan tugma bilan birga yo'qolardi (Dinamik UI'da qulaylik). Funksiya o'chirilganlar sonini qaytaradi — e'lon matni uchun.

3-mashq (qiyin): tartibni o'zgartirish

Har qatorga «Yuqoriga» va «Pastga» tugmalarini qo'shing (data-amal="yuqori" va "past"). Bosilganda vazifa qo'shnisi bilan joy almashsin, fokus o'sha tugmada qolsin, #elon esa "2-o'rinda." deb aytsin. Birinchi vazifa yuqoriga, oxirgisi pastga siljimaydi.

Ishora: splice(orin, 1) elementni olib beradi, splice(yangi, 0, vazifa) uni yangi joyga qo'yadi (Element qo'shish va olib tashlash).

Yechim

oddiyQator dagi li.append ga «Tahrirlash» dan oldin:

js
tugmaYasa("yuqori", "Yuqoriga", vazifa.matn),
tugmaYasa("past", "Pastga", vazifa.matn),

Yangi funksiyalar:

js
function vazifaniSur(id, qadam) {
  const orin = holat.vazifalar.findIndex((v) => v.id === id);
  const yangi = orin + qadam;
  if (yangi < 0 || yangi >= holat.vazifalar.length) {
    return orin;
  }
  const [vazifa] = holat.vazifalar.splice(orin, 1);
  holat.vazifalar.splice(yangi, 0, vazifa);
  render();
  return yangi;
}

function joyiniAlmashtir(id, qadam, amal) {
  const orin = vazifaniSur(id, qadam);
  fokusla(id, amal, null);
  elonQil(`${orin + 1}-o'rinda.`);
}

switch ga:

js
case "yuqori":
  joyiniAlmashtir(id, -1, amal);
  break;
case "past":
  joyiniAlmashtir(id, 1, amal);
  break;

"Non olish" ning «Pastga» tugmasida Enter — vazifa ikkinchi o'ringa tushadi, pastda "2-o'rinda.", fokus o'sha «Pastga» da. Yana ikki marta — "3-o'rinda.", ro'yxat oxirida siljish to'xtaydi. const [vazifa] = ...splice(...) — splice olib tashlanganlar massivini qaytaradi, destructuring undan birinchisini oladi (Massiv destructuring). Filtr yoqilganda qo'shni yashirin bo'lishi mumkin — u holda siljish ko'zga ko'rinmaydi. Buni tuzatishni o'zingizga qoldiramiz: masalan, tugmalarni faqat "Hammasi" filtrida chizish.

4-mashq: Portfolio qadami — loyiha kartasi

vazifalar — portfolio'ingizdagi birinchi brauzer ilovasi. Uni "Loyihalar" sahifasiga qo'shing:

  1. kurs/portfolio da yangi branch: git switch -c feature/vazifalar-karta.
  2. sayt/loyihalar/index.html dagi <div class="loyihalar"> ichiga yangi article — boshqa kartalar tuzilmasida (Yakuniy loyiha: portfolio). data-texnologiyalar ham yozing — Ko'p elementni samarali chizish darsidagi teglar paneli yangi tegni o'zi qo'shadi.
  3. Ikki havola: saytning o'zi va kodi.
  4. npm run check, commit Loyihalar: vazifalar ilovasi kartasini qo'sh, keyin PR va merge — main himoyalangan (GitHub CLI).
Yechim
html
<article data-texnologiyalar="HTML CSS JavaScript">
  <h2>Vazifalar — vanilla JS ilova</h2>
  <p><time datetime="2026-10">2026-yil oktabr</time></p>
  <p>
    Vazifa qo'shish, belgilash, tahrirlash va filtr. Holat → render,
    delegation; klaviatura va ekran o'quvchi bilan ishlaydi.
  </p>
  <ul>
    <li>
      <a href="https://login.github.io/vazifalar/">Ilovani ochish</a>
    </li>
    <li>
      <a href="https://github.com/login/vazifalar"
      >Kodni GitHub'da ko'rish</a>
    </li>
  </ul>
</article>

login — GitHub nomingiz. Boshqa kartalardagi ul da role="list" bo'lsa, bunga ham qo'shing. Teglar panelida "JavaScript" tugmasi paydo bo'ladi — uni bosing: faqat yangi karta (va 08-qismdagi byudjet kartasi, agar unga ham shu tegni yozgan bo'lsangiz) qoladi.

bash
git push -u origin feature/vazifalar-karta
gh pr create --fill
gh pr merge --squash --delete-branch

16. Real ishda

  • To-do — intervyu klassikasi. Junior frontend intervyusida "45 daqiqada kichik vazifalar ro'yxati yozing" degan amaliy topshiriq tez-tez beriladi. Kuzatiladigan narsa — aynan bugungi qarorlar: holat qayerda, delegation bormi, fokus yo'qolmaydimi, XSS'dan himoya bormi.
  • Freymvorklar shu qolipni avtomatlashtiradi. React'da render() ni o'zingiz chaqirmaysiz, fokusla kabi yordamchilar ham boshqacha yoziladi. Lekin "holat → ko'rinish" g'oyasi aynan shu. vazifalar ni 17-qismda React'da qayta yozib, ikkalasini solishtirasiz.

09-qism yakuni

Tabriklaymiz — "JavaScript brauzerda va debugging" qismi tugadi! Qism boshida konsolda null chiqib, sahifa o'zgarmagan edi. Endi brauzerda ishlaydigan, internetga chiqqan ilovangiz bor. Bu qismda:

  • DOM: elementlarni topasiz, yaratasiz, o'zgartirasiz va o'chirasiz; ko'p elementni fragment bilan chizasiz.
  • Hodisalar: tinglovchi, hodisa obyekti, bubble va delegation, klaviatura, fokus, sahifa hayot sikli.
  • Holat → render: ekran doim bitta ma'lumot manbaidan chiziladi.
  • Formalar: submit, FormData, jonli tekshiruv va o'zbekcha xato xabarlari.
  • Qulaylik: aria-live, aria-pressed, fokusni boshqarish.
  • Debugging: xatoni takrorlash, gipoteza, stack trace o'qish.

Oldinda nima bor

10-qism — "JavaScript chuqur". Bugun for...of bilan qo'lda yozgan korinadiganlar ni u yerda bitta qatorda filter bilan yozasiz. Callback, closure, class va modullar ham keladi. vazifalar reposi esa modullarga bo'linadi va o'sishda davom etadi.

Xulosa

  • Yakuniy loyiha — qismdagi qoliplarning yig'indisi: holat → render, delegation, forma tekshiruvi, fokus va jonli hududlar.
  • Ma'lumot faqat holat da; soni, filtrlangan ro'yxat — har render() da hisoblanadi.
  • Bitta ul tinglovchisi data-amal va switch bilan hamma amalni boshqaradi; dataset qiymati — satr.
  • render() fokusni o'chiradi — har amaldan keyin fokusla uni mantiqiy joyga qo'yadi.
  • Foydalanuvchi matni — faqat textContent; loyiha sayti uchun yo'llar — nisbiy.
  • v1 ma'lumotni saqlamaydi — bu 11-qismning ishi.

Keyingi dars: Callback imzosi va massivning HOF metodlari — 10-qism boshlanadi: filter, map kabi metodlarga beriladigan funksiya qanday argumentlar olishini ko'ramiz.

Manbalar

  • MDN: "Event delegation", "Element: closest()", "HTMLElement: dataset" — developer.mozilla.org
  • MDN: "ARIA live regions", "aria-pressed", "HTMLInputElement: select()" — developer.mozilla.org
  • W3C: "ARIA Authoring Practices Guide" — Button (toggle) pattern — w3.org/WAI/ARIA/apg
  • GitHub Docs: "Configuring a publishing source for your GitHub Pages site" — docs.github.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript yakuniy loyiha: brauzerda vazifalar ro'yxati (vanilla JS) — IlmHamroh