Mundarija (49)
- Bu darsda
- 1. Nega bu kerak?
- 2. Loyiha talablari
- 2.1 Ilova nima qiladi
- 2.2 Qoidalar
- 2.3 Fayllar
- 3. Reja: ma'lumot qanday aylanadi
- 4. 1-bosqich: repo va skelet
- 4.1 Repo
- 4.2 HTML
- 4.3 CSS
- 5. 2-bosqich: holat va render
- 5.1 Vazifa
- 6. 3-bosqich: vazifa qo'shish
- 6.1 Vazifa
- 7. 4-bosqich: belgilash va o'chirish — bitta tinglovchi
- 7.1 Vazifa
- 8. 5-bosqich: tahrirlash rejimi
- 8.1 Qaysi qator tahrirda — bu ham holat
- 8.2 Vazifa
- 9. 6-bosqich: filtr, hisoblagich va bo'sh holat
- 9.1 Vazifa
- 10. 7-bosqich: sifat tekshiruvi
- 10.1 XSS sinovi
- 10.2 Klaviatura sinovi
- 10.3 Accessibility yorlig'i
- 10.4 Debug: Number unutilsa
- 11. 8-bosqich: GitHub va Pages
- 11.1 Tarix
- 11.2 GitHub'ga yuborish
- 11.3 Pages
- 12. Baholash ro'yxati
- 13. Ko'p uchraydigan xatolar
- 13.1 dataset dan kelgan satr
- 13.2 Fokus body ga tushadi
- 13.3 Tinglovchi qayta chiziladigan elementda
- 13.4 Matn innerHTML bilan
- 13.5 / bilan boshlangan yo'l
- 14. Hujumchi nigohi
- 15. Mashqlar
- 1-mashq (oson): "Hammasi bajarildi"
- 2-mashq (o'rta): bajarilganlarni tozalash
- 3-mashq (qiyin): tartibni o'zgartirish
- 4-mashq: Portfolio qadami — loyiha kartasi
- 16. Real ishda
- 09-qism yakuni
- Oldinda nima bor
- Xulosa
- Manbalar
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 faqattextContentbilan. 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:
Entersaqlaydi,Escbekor qiladi, fokus to'g'ri joyga qaytadi. - Ilovani klaviatura, XSS va stack trace bilan tekshirasiz.
- Loyihani
vazifalarreposiga 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
- Qo'shish. Forma orqali yangi vazifa qo'shiladi. Bo'sh matn va ro'yxatda bor vazifa qabul qilinmaydi, xato o'zbekcha tushuntiriladi.
- Belgilash. Har vazifa yonida katak (checkbox): bajarildi yoki yo'q.
- Tahrirlash. «Tahrirlash» tugmasi qatorni maydonga aylantiradi.
Enteryoki «Saqlash» saqlaydi,Escyoki «Bekor qilish» eski matnni qaytaradi. - O'chirish. «O'chirish» vazifani ro'yxatdan olib tashlaydi.
- Filtr. "Hammasi", "Faol" va "Bajarilgan" tugmalari.
- Hisoblagich. "2 ta vazifa qoldi" — ekran o'quvchi uni o'zgarganda o'qiydi.
2.2 Qoidalar
- Holat → render. Ma'lumot faqat
holatobyektida. Har o'zgarish: holatni o'zgartirish, keyinrender()(Holatdan UI chizish darsidagi beshta qoida). - Delegation. Ro'yxatdagi hamma tugma uchun
ulga bittaclicktinglovchisi (Event delegation). - Xavfsiz matn. Foydalanuvchi yozgan matn faqat
textContentbilan 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...ofvaifbilan — massivningfiltermetodini 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:
vazifalar/
├── index.html
├── README.md
└── assets/
├── css/
│ └── vazifalar.css
└── js/
└── vazifalar.jsYo'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
holatichida 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"| HIsh 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:
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.
<!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.requiredvamaxlength="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.#elondagirole="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:
<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):
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:
holatobyekti: uchta namuna vazifa (id,matn,bajarildi) vakeyingiId.oddiyQator(vazifa)— bittali: katak, uninglabeli, «Tahrirlash» va «O'chirish» tugmalari.render()— ro'yxatni holatdan to'liq chizadi va "N ta vazifa qoldi" deb yozadi.
Yechim
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.Spaceuni o'zi belgilaydi, NVDA "belgilangan" yoki "belgilanmagan" deydi,labelni bosish ham katakni bosadi. Bularning hammasi tekin (Tanlash maydonlari). aria-labeltugmalarda. Ekranda uchta «O'chirish» bor. MalikaTabbilan yurganda "O'chirish, tugma" ni uch marta eshitadi — qaysi biri ekanini bilmaydi.aria-labelnomni "O'chirish: Non olish" qiladi. Ko'rinadigan so'z nomning boshida qoladi — ovoz bilan boshqaradigan foydalanuvchi "O'chirish ni bos" desa ham ishlaydi.setAttributematnni HTML deb o'qimaydi, shuning uchun bu yo'l ham XSS'dan xavfsiz.data-amalvadata-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.
<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:
HTMLCollection(3) [li, li.bajarildi, li]
2 ta vazifa qoldiOxirgi 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.
git add assets/js/vazifalar.js
git commit -m "Render: vazifalarni holatdan chiz"
git switch main
git merge feature/render6. 3-bosqich: vazifa qo'shish
6.1 Vazifa
Branch: feature/qoshish. Yozing:
xatoniTop(matn, vazifalar, ozId)— toza funksiya: xato bo'lsa matnini, bo'lmasanullqaytaradi. 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).vazifaQosh(matn)— holatga yangi vazifa, keyinrender().- Formaning
submittinglovchisi:preventDefault,trim, tekshiruv, xato yoki qo'shish. - Xato ko'rinishi
inputvachangedarsidagidek:aria-invalidvaaria-describedby. Mehmon tuzata boshlaganda xato darhol yo'qolsin.
Ishora: "ro'yxatda bormi?" degan savolga some javob beradi (Massivda qidirish).
Yechim
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:
xatoniTopekranga tegmaydi. U 08-qismdagixatoSababining egizagi (Yakuniy loyiha: byudjet): kirish — matn, chiqish — xato yokinull. 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,Enterbosib boraveradi. - "Kech ayblash, tez kechirish". Xato faqat yuborishda chiqadi, tuzatilishi bilan
inputuni o'chiradi (inputvachange).
<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
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(...).datasetdan kelgan qiymat doim satr:"1".findichidagiv.id === idesa1 === "1"ni solishtiradi —false.Numbersiz kod yiqiladi. Bu xatoni 7-bosqichda ataylab qilib, stack trace bilan topamiz.labelni bosish. Mehmon vazifa matnini bossa, brauzer avvallabelga, keyin katakkaclickyuboradi.labeldadata-amalyo'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).fokuslauni yangi elementga qaytaradi.top(id) ?? top(zaxiraId) ?? sarlavha— "o'zi, bo'lmasa qo'shnisi, bo'lmasa sarlavha" (??operatori). - Qo'shni oldindan topiladi.
qoshniIdo'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:
<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
Tabbilan keling vaSpacebosing — 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 —
switchda uningcasei yo'q.
Tekshirib ko'ring: Delegation o'rniga
oddiyQatorichida har tugmagaaddEventListenerqo'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 xatoDiagrammaga 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.
tahrirQatori(vazifa)— maydon (id="tahrir-maydon", ichida eski matn) va «Saqlash», «Bekor qilish» tugmalari.render():vazifa.id === holat.tahrirIdbo'lsa —tahrirQatori, aks holdaoddiyQator.- «Tahrirlash» bosilganda — fokus maydonga, matn belgilangan holda (
select()). - Saqlash:
xatoniTopbilan tekshirish (o'zini hisobga olmasdan), keyin fokus o'sha vazifaning «Tahrirlash» tugmasiga. - Bekor qilish: holat o'zgarmaydi, fokus yana «Tahrirlash» ga.
- Maydonda
Enter— saqlash,Esc— bekor qilish (Klaviatura hodisalari).isComposingpaytida tegmang.
Yechim
Holatga tahrirId: null qo'shing. render() dagi sikl ichi:
if (vazifa.id === holat.tahrirId) {
quti.append(tahrirQatori(vazifa));
} else {
quti.append(oddiyQator(vazifa));
}Yangi funksiyalar:
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:
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:
keydownham delegation. Tahrir maydoni harrender()da yangidan yaratiladi, shuning uchun tinglovchiulda turadi vaevent.target.idbilan "bu tahrir maydonimi?" deb so'raydi.isComposing. Ba'zi tillarda harf bir necha tugma bilan "yig'iladi". O'sha payt bosilganEnterharfni 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.
saqlavabekordan boshqa har amal avvaltahrirIdni tozalaydi — nega, pastdagi savolda. - Fokus qaytadi. Yopilgan narsadan fokus uni ochgan tugmaga qaytadi — Dinamik UI'da qulaylik darsidagi ikkinchi qoida.
<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
Enterbosing — yangi matn, pastda "O'zgarish saqlandi.", fokus shu qatorning «Tahrirlash» tugmasida. - Yana
Enter— tahrir qaytadan ochiladi. Biror narsa yozib,Escbosing — 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.
- Holatga
filtr: "hammasi".mosmi(vazifa, filtr)vakorinadiganlar()—for...ofvaifbilan (Holatdan UI chizish darsidagi qolip). render()faqat ko'rinadigan vazifalarni chizsin, filtr tugmalariningaria-pressedini holatdan qo'ysin.- Ro'yxat bo'sh bo'lsa,
#bosh-xabarchiqsin — har filtr uchun o'z matni. #filtrlarga bitta tinglovchi.- "Bajarilgan" filtrida yangi vazifa qo'shilsa, filtr "Hammasi" ga o'tsin — aks holda mehmon qo'shgan vazifasini ko'rmaydi.
Yechim
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:
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:
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.vazifalardan sanaladi, ekrandagililardan emas (qoidaning birinchisi: "DOM'dan so'ramang, holatdan so'rang"). - Filtr vazifani o'chirmaydi.
korinadiganlar()yangi massiv yig'adi,holat.vazifalarga tegmaydi. Filtrlangan ro'yxat — hosila ma'lumot. - Belgilangan vazifa "Faol" dan chiqib ketadi. Shuning uchun 4-bosqichdagi
fokuslaga 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:
<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:
HTMLCollection(3) [li, li.bajarildi, li]
2 ta vazifa qoldiSinab ko'ring:
- «Faol» — ikki vazifa qoldi, «Faol» tugmasi to'q yashil (
aria-pressed="true"). - "Faol" da turib "Non olish" katagini
Spacebilan 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:
/* 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:
// 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:
<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:
Tab— maydon, «Qo'shish», uchta filtr tugmasi, keyin har qatorning katagi va tugmalari. Fokus halqasi doim ko'rinadimi?- Yangi vazifa qo'shing, uni
Spacebilan belgilang, «Tahrirlash» → yangi matn →Enter, keyin «Tahrirlash» →Esc. - Oxirgi vazifani o'chiring — fokus sarlavhaga tushadimi,
bodyga 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:
<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:
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:
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:
console.log(typeof id, id); // DEBUGKatakni 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:
# 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.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:
gh repo create vazifalar --public --source=. --pushgh 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:
- Repo → Settings → Pages → Build and deployment.
- Source — Deploy from a branch, Branch —
main, papka —/ (root)→ Save. - 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
textContentvasetAttributebilan 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: anywhereuzun 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:
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
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:
tugmaYasa("yuqori", "Yuqoriga", vazifa.matn),
tugmaYasa("past", "Pastga", vazifa.matn),Yangi funksiyalar:
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:
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:
kurs/portfolioda yangi branch:git switch -c feature/vazifalar-karta.sayt/loyihalar/index.htmldagi<div class="loyihalar">ichiga yangiarticle— boshqa kartalar tuzilmasida (Yakuniy loyiha: portfolio).data-texnologiyalarham yozing — Ko'p elementni samarali chizish darsidagi teglar paneli yangi tegni o'zi qo'shadi.- Ikki havola: saytning o'zi va kodi.
npm run check, commitLoyihalar: vazifalar ilovasi kartasini qo'sh, keyin PR va merge —mainhimoyalangan (GitHub CLI).
Yechim
<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.
git push -u origin feature/vazifalar-karta
gh pr create --fill
gh pr merge --squash --delete-branch16. 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,fokuslakabi yordamchilar ham boshqacha yoziladi. Lekin "holat → ko'rinish" g'oyasi aynan shu.vazifalarni 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
holatda; soni, filtrlangan ro'yxat — harrender()da hisoblanadi. - Bitta
ultinglovchisidata-amalvaswitchbilan hamma amalni boshqaradi;datasetqiymati — satr. render()fokusni o'chiradi — har amaldan keyinfokuslauni 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!