Mundarija (36)
- Bu darsda
- 1. Nega bu kerak?
- 2. Hodisalar: kim, qachon
- 2.1 Ikki ishtirokchi
- 2.2 Nimani sudrash mumkin
- 2.3 Eng muhim qoida: dragover da preventDefault()
- 3. «Bahor»: bugungi taklif
- 3.1 Jonli misol
- 3.2 Qatorma-qator
- 3.3 dataTransfer: sudrash "chamadoni"
- 3.4 effectAllowed va dropEffect mos kelmasa
- 4. Fayllarni tashlash zonasi
- 4.1 Fayl — boshqacha manba
- 4.2 «Bahor»: cheklarni tashlash
- 4.3 Kod qanday ishlaydi
- 4.4 dragleave "miltillashi"
- 4.5 Papkalar
- 5. Telefon va klaviatura: DnD API'ning chegarasi
- 5.1 Uchta muammo
- 5.2 «Bahor»: menyu tartibi Pointer Events bilan
- 5.3 Kod qanday ishlaydi
- 6. Hujumchi nigohi
- 7. Ko'p uchraydigan xatolar
- 7.1 dragover da preventDefault() yo'q
- 7.2 Zonadan tashqariga tashlangan faylni unutish
- 7.3 dragover da getData()
- 7.4 effectAllowed va dropEffect mos emas
- 7.5 Faqat sudrash
- 8. Mashqlar
- 1-mashq (oson): Savatga sudrash
- 2-mashq (o'rta): Faqat rasmlar
- 3-mashq (qiyin): Buyurtmalar taxtasi
- 4-mashq: Vazifalar qadami — faylni sudrab tashlab import
- 9. Real ishda
- Xulosa
- Manbalar
JavaScript Drag and Drop API: elementlar va fayllarni sudrab tashlash
Qisqacha: Brauzerning Drag and Drop API'si bilan element (
draggable="true") yoki kompyuterdagi fayl sichqoncha bilan sudrab, boshqa joyga tashlanadi. Manbadragstartdaevent.dataTransfer.setData()bilan ma'lumot beradi, nishon esadragoverdapreventDefault()qilib "bu yerga tashlash mumkin" deydi vadropda ma'lumotni (getData()yokidataTransfer.files) oladi. API telefonda ishonchsiz va klaviaturada umuman ishlamaydi — shuning uchun doim muqobil yo'l (tugma, fayl tanlash maydoni yoki Pointer Events) qo'shiladi.
Bu darsda
draggable,dragstart,dragover,drop,dragendhodisalarini to'g'ri tartibda ulay olasiz.dataTransferorqali ma'lumot uzatasiz vaeffectAllowed/dropEffectbilan kursorni boshqarasiz.- Fayllarni sahifaga tashlash zonasini (drop zone) yasaysiz.
- Ro'yxatni sudrab tartiblashni Pointer Events bilan yozasiz — telefon va klaviatura uchun ham.
vazifalarga eksport faylini sudrab tashlab import qilishni qo'shasiz.
Oldin bilishingiz kerak: Fayl yuklash (upload) va progress, Faylni o'qish va oldindan ko'rish, Sichqoncha, pointer va touch hodisalari, Event delegation.
1. Nega bu kerak?
«Bahor» admin sahifasida ikki iltimos bor.
Birinchisi — Jasur akadan: "Har kuni ertalab bitta taomni «Bugungi taklif» qilaman. Menyudan taomni olib, shu qutiga tashlasam bo'ldi". Ro'yxatdan tanlab, tugmani bosish ham mumkin, lekin "olib tashlash" — qo'lning tabiiy harakati.
Ikkinchisi — kassir opadan. U kun oxirida cheklarning rasmlarini yuklaydi. Har safar «Fayl tanlash» → papka → fayl → «Ochish». Fayllar allaqachon ochiq papkada turibdi — ularni shunchaki sahifaga tashlab yuborsa qulay bo'lardi. Hozir esa fayl tashlansa, brauzer uni ochib yuboradi va admin sahifasi yopiladi.
Bu ikkala holat uchun brauzerda tayyor API bor: Drag and Drop (sudrab tashlash, qisqasi DnD). Sichqoncha va pointer hodisalari darsida "fayllarni brauzerga tashlash uchun alohida API bor" degan edik. Bugun uni ko'ramiz — va qayerda u yetmasligini ham.
2. Hodisalar: kim, qachon
2.1 Ikki ishtirokchi
Sudrashda ikki tomon bor:
- Manba (source) — sudralayotgan narsa: element, havola, rasm yoki kompyuterdagi fayl.
- Nishon (drop target) — tashlanadigan joy.
Har biri o'z hodisalarini oladi:
| Hodisa | Kimga | Qachon |
|---|---|---|
dragstart |
manba | sudrash boshlandi |
drag |
manba | sudrash davomida (tez-tez) |
dragenter |
nishon | kursor nishon ustiga kirdi |
dragover |
nishon | kursor nishon ustida (takror-takror) |
dragleave |
nishon | kursor nishondan chiqdi |
drop |
nishon | tashlandi |
dragend |
manba | sudrash tugadi (tashlandi yoki bekor) |
Kompyuterdagi fayl sudralganda manba — brauzerdan tashqarida. Shuning uchun dragstart va dragend kelmaydi, faqat nishon hodisalari.
sequenceDiagram
participant M as Manba (li)
participant N as Nishon (quti)
Note over M: dragstart — setData()
M->>N: kursor kirdi
Note over N: dragenter
Note over N: dragover × N — preventDefault()
M->>N: sichqoncha qo'yib yuborildi
Note over N: drop — getData()
Note over M: dragend — dropEffect2.2 Nimani sudrash mumkin
Ba'zi narsalar o'zi sudraladi: havolalar (<a href>), rasmlar (<img>) va belgilangan matn. Boshqa elementni sudraladigan qilish uchun — draggable="true" atributi:
<li draggable="true">Osh</li>Teskarisi ham bor: rasmni sudralmaydigan qilish — <img draggable="false">. Masalan, galereyada rasm ustida sichqoncha bilan ishlaganda brauzer rasmni "ko'chirib" ketmasin.
2.3 Eng muhim qoida: dragover da preventDefault()
Sukut bo'yicha sahifadagi deyarli hech bir element tashlashni qabul qilmaydi. Kursor "taqiq" belgisiga aylanadi va drop hodisasi umuman kelmaydi. Element "bu yerga tashlash mumkin" deyishi uchun dragover hodisasida standart harakatni bekor qilishi kerak:
zone.addEventListener("dragover", (event) => {
event.preventDefault(); // "ha, bu yerga tashlash mumkin"
});Bu teskari mantiqqa o'xshaydi: "standart harakatni to'xtat" = "ruxsat ber". Aslida mantiq to'g'ri. Brauzerning standart harakati — rad etish. Uni bekor qilsangiz, rad etilmaydi (Event obyekti va standart harakatlar darsidagi preventDefault).
Va drop da ham preventDefault() — bu boshqa sabab bilan: aks holda brauzer o'zining standart ishini qiladi. Fayl bo'lsa — uni ochadi, havola bo'lsa — unga o'tadi.
Tekshirib ko'ring: Sardor faqat
droptinglovchisini yozdi,dragoverni yozmadi. Element tashlansa nima bo'ladi?
Javob
Hech narsa: drop hodisasi kelmaydi. dragover da preventDefault() bo'lmasa, brauzer nishonni "qabul qilmaydi" deb hisoblaydi va kursor taqiq belgisini ko'rsatadi. Bu DnD'dagi eng ko'p uchraydigan xato.
3. «Bahor»: bugungi taklif
3.1 Jonli misol
Menyudan taomni «Bugungi taklif» qutisiga sudrang:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
#menu { list-style: none; padding: 0; display: flex; gap: 0.5rem;
flex-wrap: wrap; }
#menu li { padding: 0.4rem 0.8rem; border: 1px solid #9bb59b;
border-radius: 0.5rem; background: #fff; cursor: grab; }
#menu li.dragging { opacity: 0.4; }
#offer { margin-top: 1rem; padding: 1rem; border-radius: 0.75rem;
border: 2px dashed #9bb59b; }
#offer.over { border-color: #2e7d32; background: #eef7ee; }
</style>
<ul id="menu">
<li id="osh" draggable="true" data-dish="Osh">Osh</li>
<li id="manti" draggable="true" data-dish="Manti">Manti</li>
<li id="lagmon" draggable="true" data-dish="Lag'mon">Lag'mon</li>
</ul>
<div id="offer">Bugungi taklif: <strong id="dish">—</strong></div>
<script>
const menu = document.getElementById("menu");
const offer = document.getElementById("offer");
menu.addEventListener("dragstart", (event) => {
const item = event.target.closest("li");
event.dataTransfer.setData("text/plain", item.dataset.dish);
event.dataTransfer.effectAllowed = "copy";
item.classList.add("dragging");
console.log(`dragstart: ${item.dataset.dish}`);
});
menu.addEventListener("dragend", (event) => {
event.target.classList.remove("dragging");
console.log(`dragend: ${event.dataTransfer.dropEffect}`);
});
offer.addEventListener("dragenter", (event) => {
event.preventDefault();
offer.classList.add("over");
});
offer.addEventListener("dragover", (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
});
offer.addEventListener("dragleave", (event) => {
if (!offer.contains(event.relatedTarget)) {
offer.classList.remove("over");
}
});
offer.addEventListener("drop", (event) => {
event.preventDefault();
offer.classList.remove("over");
const dish = event.dataTransfer.getData("text/plain");
document.getElementById("dish").textContent = dish;
console.log(`drop: ${dish}`);
});
</script>Biz Chrome 154 da haqiqiy sichqoncha harakati bilan (bosish → surish → qo'yib yuborish) «Osh» ni qutiga sudradik. Konsolda:
dragstart: Osh
drop: Osh
dragend: copyQuti ichida Bugungi taklif: Osh paydo bo'ldi. Qutiga yetkazmay, bo'sh joyda qo'yib yuborganimizda esa drop kelmadi va oxirgi qator dragend: none bo'ldi.
3.2 Qatorma-qator
menu.addEventListener("dragstart", …)— tinglovchi har<li>ga emas, ro'yxatga bitta (Event delegation).dragstartko'tariladi (bubble),event.target— sudralgan<li>.event.dataTransfer.setData("text/plain", …)— sudrash bilan birga ketadigan ma'lumot. Birinchi argument — tur (MIME ko'rinishida), ikkinchisi — satr. Nishon aynan shu turni so'raydi.effectAllowed = "copy"— manba "meni nusxalash mumkin" deydi.dropEffect = "copy"— nishon "men nusxalayman" deydi. Ikkalasi mos kelsa, kursor yonida+belgisi chiqadi..draggingva.overklasslari — foydalanuvchiga ko'rinadigan javob: nima sudralyapti va qayerga tashlash mumkin.dragleavedagicontains(event.relatedTarget)— pastda alohida bo'limda.dropdagetData("text/plain")—dragstartda qo'yilgan o'sha satr.textContentbilan yoziladi.dragenddagidropEffect— natija:"copy"(tashlandi) yoki"none"(bekor qilindi, Esc yoki noto'g'ri joy).
3.3 dataTransfer: sudrash "chamadoni"
event.dataTransfer — sudrash davomida qo'lda ko'tarib yurilgan chamadon. Manba uni to'ldiradi, nishon ochadi. Asosiy a'zolari:
| A'zo | Nima |
|---|---|
setData(tur, satr) / getData(tur) |
ma'lumot qo'yish / olish |
types |
ichidagi turlar ro'yxati |
files |
sudralgan fayllar (FileList) |
effectAllowed / dropEffect |
copy, move, link, none |
Bir nechta tur qo'yish mumkin: text/plain — hamma tushunadigan oddiy matn, yana o'zingizniki, masalan application/x-bahor-dish — JSON bilan. Havola sudralganda brauzer o'zi uchta tur qo'yadi. Chrome 154 da sinovda: text/uri-list, text/plain, text/html. Chrome yana o'zining ichki turini ham qo'shadi (chromium/x-drag-id) — unga e'tibor bermang.
Himoyalangan rejim. getData() faqat drop da ishlaydi. dragenter va dragover da u bo'sh satr qaytaradi — sinovda dragover ichida getData("text/plain") → "". Bu xavfsizlik uchun: kursor sahifangiz ustidan o'tib ketayotganda, boshqa saytdan sudralgan matnni o'qib ololmaysiz. Turlarni (types) esa ko'rish mumkin — "bu yerga faqat fayl qabul qilaman" degan qaror uchun shu yetadi.
3.4 effectAllowed va dropEffect mos kelmasa
Sardor manbada effectAllowed = "move" yozdi, nishonda esa dropEffect = "copy" qoldi. Sinovda tashlash ishlamadi: drop kelmadi, dragend da none. Brauzer ikki tomon "kelishmagan" deb, tashlashni rad etdi. Ikkalasini bir xil qiling yoki dropEffect ni umuman yozmang — unda brauzer effectAllowed dan o'zi tanlaydi.
Tekshirib ko'ring:
dragoverichidaevent.dataTransfer.getData("text/plain")ni o'qib, "faqat Osh bo'lsa ruxsat ber" demoqchi bo'ldingiz. Nega ishlamaydi va qanday yechiladi?
Javob
dragover da ma'lumot himoyalangan — getData bo'sh satr qaytaradi. Yechim: ma'lumotni tur nomiga joylash, masalan setData("application/x-dish-osh", ""), keyin dragover da event.dataTransfer.types.includes("application/x-dish-osh") ni tekshirish. Turlar ro'yxati himoyalanmagan.
4. Fayllarni tashlash zonasi
4.1 Fayl — boshqacha manba
Kompyuterdagi fayl sudralganda dataTransfer.types da "Files" bo'ladi. Fayllarning o'zi — dataTransfer.files da, xuddi input.files dagi FileList (File va Blob). Ya'ni tashlangan faylni o'qish va yuklash — o'tgan uch darsdagi kodning o'zi.
Bo'sh joyni to'ldiring: kompyuterdan fayl sudralayotganini dragover da shunday bilamiz — event.dataTransfer.types.includes("")
4.2 «Bahor»: cheklarni tashlash
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
#zone { padding: 1.5rem 1rem; text-align: center;
border: 2px dashed #9bb59b; border-radius: 0.75rem; }
#zone.over { border-color: #2e7d32; background: #eef7ee; }
#zone p { margin: 0.25rem 0; }
</style>
<div id="zone">
<p>Cheklarni shu yerga tashlang</p>
<p>yoki
<label for="pick">fayl tanlang</label>:
<input type="file" id="pick" multiple>
</p>
</div>
<ul id="list" aria-live="polite"></ul>
<script>
const zone = document.getElementById("zone");
const pick = document.getElementById("pick");
const list = document.getElementById("list");
const hasFiles = (event) =>
event.dataTransfer.types.includes("Files");
function showFiles(files) {
for (const file of files) {
const item = document.createElement("li");
item.textContent = `${file.name} — ${file.size} bayt`;
list.append(item);
}
console.log(`Qabul qilindi: ${files.length} ta fayl`);
}
zone.addEventListener("dragover", (event) => {
if (!hasFiles(event)) return;
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
zone.classList.add("over");
});
zone.addEventListener("dragleave", (event) => {
if (!zone.contains(event.relatedTarget)) {
zone.classList.remove("over");
}
});
zone.addEventListener("drop", (event) => {
if (!hasFiles(event)) return;
event.preventDefault();
zone.classList.remove("over");
showFiles(event.dataTransfer.files);
});
pick.addEventListener("change", () => {
showFiles(pick.files);
pick.value = "";
});
// Zonadan tashqariga tashlangan faylni brauzer ochmasin
for (const type of ["dragover", "drop"]) {
window.addEventListener(type, (event) => {
if (hasFiles(event)) event.preventDefault();
});
}
</script>Biz sahifa ichida DataTransfer va File yaratib, zonaga dragenter → dragover → drop hodisalarini yubordik: chek-1.png (2 048 bayt) va chek-2.jpg (1 024 bayt). Ro'yxatda ikkala fayl chiqdi, konsolda Qabul qilindi: 2 ta fayl. Faylni zonadan tashqarida, sahifaning boshqa joyiga tashladik — drop bekor qilindi (brauzer faylni ochmadi). Fayl tanlash maydoni orqali ham xuddi shu natija.
Kompyuteringizda sinab ko'ring: Explorer yoki Finder'dan bir nechta faylni natija oynasidagi zonaga sudrang. Fayllar hech qayerga yuborilmaydi — faqat nomi va hajmi ko'rsatiladi.
4.3 Kod qanday ishlaydi
hasFiles(event)— faqat fayllar bilan ishlaymiz. Sahifadagi matn yoki havola zonaga sudralsa,preventDefaultqilinmaydi — zona ularni rad etadi.zone.classList.add("over")dragoverda —dragenterda emas.dragoverkursor qimirlaganda va turganda ham takror-takror keladi,classList.addesa bor klassni qayta qo'shmaydi, ortiqcha ish yo'q.- Fayl tanlash maydoni — majburiy muqobil. Klaviatura bilan ishlaydigan Bobur, ekran o'quvchili Malika va telefondagi mehmonlar faylni sudray olmaydi. Ikkala yo'l bitta
showFilesga boradi. windowdagi qo'riqchi — eng ko'p unutiladigan qism. Fayl zonaga 5 px yetmay tashlansa, brauzer uni ochadi: PDF ko'rsatiladi, rasm ochiladi, sahifa — va to'ldirilgan forma — yo'qoladi. Ikki tinglovchi faqat fayllar uchun standartni bekor qiladi. Matnni sudrab maydonga tashlash odatdagidek ishlaydi.
4.4 dragleave "miltillashi"
dragleave faqat zonadan chiqqanda emas, zona ichidagi bola elementga o'tganda ham keladi. Kursor <p> ustiga o'tdi — zona dragleave oladi, <p> esa dragenter. dragleave da shunchaki classList.remove("over") yozsangiz, zona ramkasi ichki elementlar ustida "miltillaydi".
Yechim — event.relatedTarget: kursor qayerga o'tdi. U zonaning ichida bo'lsa (zone.contains(...)), zonadan chiqilmagan — klassni olib tashlamaymiz. Sichqoncha hodisalari darsidagi mouseover/mouseenter farqini eslang — muammo o'sha.
Ba'zi kodlarda boshqa usulni ko'rasiz: dragenter da hisoblagichni oshirish, dragleave da kamaytirish, nolga tushsa — chiqildi. U ham ishlaydi, lekin hisoblagich adashib qolishi oson.
Tekshirib ko'ring: Kassir opa chekni zonadan 5 px chetroqqa tashladi. Kodda
windowdagi qo'riqchi yo'q. Nima bo'ladi?
Javob
Brauzer faylni o'zi ochadi: rasm yoki PDF butun oynada ko'rinadi, admin sahifasi esa yopiladi — to'ldirilgan forma bilan birga. Zona tashqarisida hech kim preventDefault() qilmagani uchun brauzer standart ishini bajardi. Qo'riqchi aynan shu standartni faqat fayllar uchun bekor qiladi.
Maslahat: Butun sahifani zona qilish ham mumkin: fayl oynaga kirganda yarim shaffof qoplama chiqadi ("Faylni tashlang"). Gmail, Telegram Web shunday qiladi. Unda tinglovchilar
documentga ulanadi.
4.5 Papkalar
Papka tashlansa, dataTransfer.files da u bitta g'alati "fayl" bo'lib keladi (turi bo'sh, hajmi ma'nosiz) — ichidagilar emas. Papka ichini o'qish uchun dataTransfer.items va item.webkitGetAsEntry() kerak: u fayl yoki papka "yozuvi"ni beradi va papkani aylanib chiqish mumkin. Nomida webkit bor, lekin hamma asosiy brauzerlarda ishlaydi. Bu kam kerak bo'ladi — bilib qo'ysangiz yetadi.
5. Telefon va klaviatura: DnD API'ning chegarasi
5.1 Uchta muammo
HTML Drag and Drop sichqoncha uchun loyihalangan. Real loyihada uchta muammo chiqadi:
- Telefon. Sensorli ekranda HTML DnD ishonchsiz: ba'zi mobil brauzerlarda umuman ishlamaydi, ba'zilarida faqat uzoq bosishdan keyin va boshqacha xulq bilan. «Bahor» mehmonlarining aksariyati telefonda.
- Klaviatura. Sudrashni klaviaturadan boshlashning standart yo'li yo'q. Bobur ham, Malika ham bu funksiyadan foydalana olmaydi.
- Ko'rinish. Sudralayotgan elementning "sharpasi"ni brauzer chizadi.
setDragImage()bilan rasmini almashtirish mumkin, lekin uni CSS bilan animatsiya qilib, ro'yxatdagi boshqa elementlarni silliq siljitib bo'lmaydi.
Shuning uchun:
- Fayl tashlash — HTML DnD to'g'ri tanlov (fayl tashqaridan keladi, boshqa yo'l yo'q). Muqobili — fayl tanlash maydoni.
- Sahifa ichida tartiblash (ro'yxat, kanban) — ko'pincha Pointer Events bilan o'zingiz yozasiz va klaviatura uchun tugmalar qo'shasiz.
5.2 «Bahor»: menyu tartibi Pointer Events bilan
Jasur aka menyudagi taomlar tartibini o'zgartirmoqchi. Har qatorda ushlash belgisi () — uni sudrang. Klaviaturadan — «↑» va «↓» tugmalari:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
#dishes { list-style: none; padding: 0; max-width: 22rem; }
#dishes li { display: flex; align-items: center; gap: 0.5rem;
padding: 0.4rem 0.5rem; margin: 0.3rem 0; background: #fff;
border: 1px solid #9bb59b; border-radius: 0.5rem; }
#dishes li.dragging { box-shadow: 0 4px 12px #0003; }
.handle { cursor: grab; touch-action: none; padding: 0 0.25rem;
user-select: none; }
.name { flex: 1; }
#dishes button { font: inherit; padding: 0.1rem 0.5rem; }
</style>
<ul id="dishes">
<li id="d1"><span class="handle" aria-hidden="true">☰</span>
<span class="name">Osh</span>
<button data-move="-1" aria-label="Osh — yuqoriga">↑</button>
<button data-move="1" aria-label="Osh — pastga">↓</button></li>
<li id="d2"><span class="handle" aria-hidden="true">☰</span>
<span class="name">Manti</span>
<button data-move="-1" aria-label="Manti — yuqoriga">↑</button>
<button data-move="1" aria-label="Manti — pastga">↓</button></li>
<li id="d3"><span class="handle" aria-hidden="true">☰</span>
<span class="name">Lag'mon</span>
<button data-move="-1" aria-label="Lag'mon — yuqoriga">↑</button>
<button data-move="1" aria-label="Lag'mon — pastga">↓</button>
</li>
</ul>
<p id="announce" aria-live="polite"></p>
<script>
const list = document.getElementById("dishes");
const announce = document.getElementById("announce");
function reportOrder(item) {
const names = [...list.querySelectorAll(".name")]
.map((name) => name.textContent);
const name = item.querySelector(".name").textContent;
const place = names.indexOf(name);
announce.textContent = `${name} — ${place + 1}-o'rinda`;
console.log(names.join(", "));
}
// Sichqoncha va barmoq: Pointer Events
list.addEventListener("pointerdown", (event) => {
const handle = event.target.closest(".handle");
if (handle === null) return;
const item = handle.closest("li");
item.classList.add("dragging");
const controller = new AbortController();
function onMove(moveEvent) {
const under = document
.elementFromPoint(moveEvent.clientX, moveEvent.clientY)
?.closest("#dishes li");
if (!under || under === item) return;
const box = under.getBoundingClientRect();
const below = moveEvent.clientY > box.top + box.height / 2;
if (below) under.after(item);
else under.before(item);
}
function onEnd() {
controller.abort(); // uchala tinglovchi birdan o'chadi
item.classList.remove("dragging");
reportOrder(item);
}
const options = { signal: controller.signal };
document.addEventListener("pointermove", onMove, options);
document.addEventListener("pointerup", onEnd, options);
document.addEventListener("pointercancel", onEnd, options);
});
// Klaviatura va ekran o'quvchi: tugmalar
list.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (button === null) return;
const item = button.closest("li");
if (button.dataset.move === "-1") {
item.previousElementSibling?.before(item);
} else {
item.nextElementSibling?.after(item);
}
button.focus(); // ko'chgandan keyin ham fokus shu tugmada
reportOrder(item);
});
</script>Biz Chrome 154 da sinadik. «Osh» ning belgisini sichqoncha bilan «Lag'mon» qatorining o'rtasigacha sudradik — konsolda Manti, Osh, Lag'mon. «Osh» bir pog'ona tushdi, lekin «Lag'mon» dan o'tmadi: kursor uning pastki yarmiga yetmadi. Pastroq tushirsangiz — oxiriga o'tadi. Keyin «Manti — pastga» tugmasini bosdik — Osh, Manti, Lag'mon, e'lon matni Manti — 2-o'rinda, fokus o'sha tugmada qoldi.
5.3 Kod qanday ishlaydi
- Tinglovchilar
documentda,setPointerCapturesiz. Sichqoncha va pointer hodisalari darsida sudrash uchunsetPointerCaptureni ishlatgan edik. Bu yerda u ishlamaydi va biz buni sinovda ko'rdik.under.after(item)elementni DOM'da ko'chiradi, ko'chirish esa "o'chirib, qayta qo'yish" degani. O'chirilgan element pointer'ni ushlab turolmaydi — capture yo'qoladi. Birinchi sinovdapointerupboshqa elementga ketdi va sudrash tugamay qoldi.documentesa hech qayerga ko'chmaydi: hodisalar ko'tarilib (bubble) baribir unga yetadi. AbortControllerbilan tozalash — Listener opsiyalari darsidagisignal: bittaabort()uchala tinglovchini o'chiradi.touch-action: none— eng muhim CSS qatori. Busiz telefonda barmoq bilan surish sahifani aylantiradi va brauzerpointercancelyuboradi. Faqat ushlash belgisida — qolgan joyda sahifa odatdagidek aylanadi.document.elementFromPoint(x, y)— "shu nuqtada qaysi element?" Kursor ostidagi<li>ni topamiz. U yuqori yarmida bo'lsa — sudralayotgan element undan oldinga, pastki yarmida — undan keyinga ko'chadi.pointercancel— tizim sudrashni to'xtatsa (qo'ng'iroq keldi, ekran burildi) —pointerupkabi yakunlaymiz.- Tugmalar — DnD'siz to'liq muqobil.
aria-labelekran o'quvchi uchun aniq nom beradi ("Osh — yuqoriga"),aria-liveesa natijani aytadi. Bu WCAG talabi: sudrash bilan qilinadigan har ish oddiy bosish bilan ham qilinishi kerak.
Bu ~70 qatorlik oddiy versiya. Haqiqiy loyihalarda silliq animatsiya, avtomatik aylantirish (ro'yxat ekrandan uzun bo'lsa) va klaviatura bilan "ushlab olib ko'chirish" kerak bo'ladi. Buni SortableJS yoki React uchun dnd-kit kabi kutubxonalar beradi.
Tekshirib ko'ring:
.handledantouch-action: noneolib tashlansa, telefonda nima bo'ladi?
Javob
Barmoq ushlash belgisini surganda brauzer buni "sahifani aylantirish" deb tushunadi: sahifa aylanadi, bir lahzadan keyin pointercancel keladi va sudrash to'xtaydi. Element joyidan qimirlamaydi. touch-action: none brauzerga "bu yerda barmoq harakati — meniki" deydi.
6. Hujumchi nigohi
Sahifangizga tashqaridan nima tashlanishi mumkin — siz tanlamaysiz. Boshqa saytdagi matn, havola, HTML bo'lagi — hammasi drop ga keladi.
Hujum: tashlangan HTML. Hujumchi o'z sahifasida "chiroyli" matn tayyorlaydi va foydalanuvchini uni sizning izoh maydoningizga sudrashga undaydi. text/html turida <img src=x onerror="…"> keladi. Sardor uni innerHTML bilan qo'ysa — XSS.
Himoya:
getData("text/plain")ni ishlating va natijanitextContentbilan chiqaring.text/htmlfaqat haqiqatan formatlangan matn kerak bo'lganda — va unda tozalash (sanitizer) bilan (XSS va DOM xavfsizligi, shu qismda).text/uri-listdan kelgan havolani<a href>ga qo'yishdan oldin protokolini tekshiring:javascript:bilan boshlanadigan "havola" bosilganda kod bajaradi.new URL(link).protocol—"https:"yoki"http:"bo'lsin (URL va URLSearchParams).- Tashlangan fayllar — Faylni o'qish darsidagi tekshiruvlar (hajm, sehrli baytlar) va server tekshiruvi.
7. Ko'p uchraydigan xatolar
7.1 dragover da preventDefault() yo'q
drop kelmaydi. Tuzatish: nishonning dragover ida event.preventDefault().
7.2 Zonadan tashqariga tashlangan faylni unutish
Brauzer faylni ochadi, sahifa yopiladi. Tuzatish: window da fayllar uchun dragover va drop ni bekor qiling.
7.3 dragover da getData()
Bo'sh satr. Tuzatish: qaror uchun types, ma'lumot uchun — drop.
7.4 effectAllowed va dropEffect mos emas
move va copy — drop kelmaydi. Tuzatish: bir xil qiymat yoki dropEffect ni yozmaslik.
7.5 Faqat sudrash
Telefon va klaviatura foydalanuvchilari funksiyadan mahrum. Tuzatish: fayl tanlash maydoni, tugmalar yoki Pointer Events.
8. Mashqlar
1-mashq (oson): Savatga sudrash
Uchta taom kartasini (draggable="true") savat belgisiga sudrash mumkin bo'lsin. Har tashlashda savatdagi son bittaga oshsin va konsolga Savatga: <taom nomi> chiqsin. «Bugungi taklif» misolidagi naqshni ishlating.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.dish { display: inline-block; padding: 0.4rem 0.8rem;
margin: 0.25rem; border: 1px solid #9bb59b;
border-radius: 0.5rem; cursor: grab; }
#cart { margin-top: 1rem; padding: 1rem; font-size: 1.25rem;
border: 2px dashed #9bb59b; border-radius: 0.75rem; }
</style>
<span class="dish" id="p" draggable="true">Osh</span>
<span class="dish" id="m" draggable="true">Manti</span>
<span class="dish" id="s" draggable="true">Somsa</span>
<div id="cart">🛒 <span id="count">0</span></div>
<script>
const cart = document.getElementById("cart");
const count = document.getElementById("count");
document.addEventListener("dragstart", (event) => {
if (!event.target.classList?.contains("dish")) return;
const dish = event.target.textContent;
event.dataTransfer.setData("text/plain", dish);
event.dataTransfer.effectAllowed = "copy";
});
cart.addEventListener("dragover", (event) => {
event.preventDefault();
});
cart.addEventListener("drop", (event) => {
event.preventDefault();
const dish = event.dataTransfer.getData("text/plain");
count.textContent = Number(count.textContent) + 1;
console.log(`Savatga: ${dish}`);
});
</script>dropEffect yozilmadi — brauzer effectAllowed dagi copy ni oladi. classList?. — sahifadagi matn sudralganda event.target matn tuguni bo'lishi mumkin, unda classList yo'q. Sinovda «Manti» ni savatga sudradik: Savatga: Manti, son — 1. Real ishda albatta «Savatga» tugmasi ham bo'ladi.
2-mashq (o'rta): Faqat rasmlar
Tashlash zonasi faqat rasmlarni qabul qilsin: har rasm kichik <img> bo'lib chiqsin (obyekt URL, yuklangach revoke). Rasm bo'lmagan fayllar uchun konsolga O'tkazib yuborildi: <nomi>. Fayl tanlash muqobili ham bo'lsin.
Ishora: file.type.startsWith("image/") (File va Blob dagi galereya mashqi), qolgani — "Fayllarni tashlash zonasi" bo'limidagi kod.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
#zone { padding: 1rem; border: 2px dashed #9bb59b;
border-radius: 0.75rem; }
#zone.over { background: #eef7ee; }
#thumbs img { width: 80px; height: 60px; object-fit: cover;
margin: 0.25rem; }
</style>
<div id="zone">
Rasmlarni tashlang yoki
<label for="pick">tanlang</label>
<input type="file" id="pick" accept="image/*" multiple>
</div>
<div id="thumbs"></div>
<script>
const zone = document.getElementById("zone");
const pick = document.getElementById("pick");
const thumbs = document.getElementById("thumbs");
const hasFiles = (event) =>
event.dataTransfer.types.includes("Files");
function addImages(files) {
for (const file of files) {
if (!file.type.startsWith("image/")) {
console.log(`O'tkazib yuborildi: ${file.name}`);
continue;
}
const img = document.createElement("img");
const url = URL.createObjectURL(file);
img.addEventListener("load", () => URL.revokeObjectURL(url),
{ once: true });
img.src = url;
img.alt = file.name;
thumbs.append(img);
}
}
zone.addEventListener("dragover", (event) => {
if (!hasFiles(event)) return;
event.preventDefault();
zone.classList.add("over");
});
zone.addEventListener("dragleave", (event) => {
if (!zone.contains(event.relatedTarget)) {
zone.classList.remove("over");
}
});
zone.addEventListener("drop", (event) => {
if (!hasFiles(event)) return;
event.preventDefault();
zone.classList.remove("over");
addImages(event.dataTransfer.files);
});
pick.addEventListener("change", () => addImages(pick.files));
for (const type of ["dragover", "drop"]) {
window.addEventListener(type, (event) => {
if (hasFiles(event)) event.preventDefault();
});
}
</script>Biz osh.png va izoh.txt ni fayl tanlash maydoni orqali berdik: bitta rasm chiqdi, konsolda O'tkazib yuborildi: izoh.txt. Zonaga ikki rasm va bitta matn fayli tashlaganimizda ham xuddi shunday. addImages — ikki yo'lning umumiy qismi.
3-mashq (qiyin): Buyurtmalar taxtasi
Ikki ustun: «Tayyorlanmoqda» va «Tayyor». Buyurtma kartalarini (№ 17 — osh, № 18 — manti) birinchisidan ikkinchisiga ko'chirish (move) mumkin bo'lsin. Har kartada «Tayyor» tugmasi ham bo'lsin — klaviatura muqobili. Ko'chganda konsolga: № 17 — osh: Tayyor.
Ishora: setData ga kartaning id sini yozing, drop da document.getElementById(id) ni toping va append qiling. effectAllowed va dropEffect — ikkalasi "move".
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.board { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.column { min-height: 8rem; padding: 0.5rem; background: #f3f6f3;
border-radius: 0.75rem; }
.column h3 { margin: 0 0 0.5rem; font-size: 1rem; }
.card { padding: 0.4rem; margin: 0.3rem 0; background: #fff;
border: 1px solid #9bb59b; border-radius: 0.5rem; cursor: grab; }
.card button { font: inherit; font-size: 0.85rem; }
#done .card button { display: none; }
</style>
<div class="board">
<section class="column" id="cooking" aria-labelledby="h1">
<h3 id="h1">Tayyorlanmoqda</h3>
<div class="card" id="o17" draggable="true">№ 17 — osh
<button type="button">Tayyor</button></div>
<div class="card" id="o18" draggable="true">№ 18 — manti
<button type="button">Tayyor</button></div>
</section>
<section class="column" id="done" aria-labelledby="h2">
<h3 id="h2">Tayyor</h3>
</section>
</div>
<script>
const done = document.getElementById("done");
function markDone(card) {
done.append(card);
const title = card.firstChild.textContent.trim();
console.log(`${title}: Tayyor`);
}
document.addEventListener("dragstart", (event) => {
const card = event.target.closest?.(".card");
if (!card) return;
event.dataTransfer.setData("text/plain", card.id);
event.dataTransfer.effectAllowed = "move";
});
done.addEventListener("dragover", (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
});
done.addEventListener("drop", (event) => {
event.preventDefault();
const id = event.dataTransfer.getData("text/plain");
const card = document.getElementById(id);
if (card?.classList.contains("card")) markDone(card);
});
document.getElementById("cooking").addEventListener("click",
(event) => {
const button = event.target.closest("button");
if (button) markDone(button.closest(".card"));
});
</script>card?.classList.contains("card") — tekshiruv: drop ga boshqa sahifadan istalgan matn kelishi mumkin. Agar u tasodifan sahifangizdagi biror id ga mos kelsa, o'sha element "ko'chib" ketmasin. Biz sinadik: № 17 ni sichqoncha bilan «Tayyor» ga sudradik — № 17 — osh: Tayyor; № 18 dagi tugmani bosdik — № 18 — manti: Tayyor.
4-mashq: Vazifalar qadami — faylni sudrab tashlab import
O'tgan darslarda vazifalar fayldan import qilishni o'rgandi: «Yoki eksport faylini tanlang» maydoni. Endi eksport faylini Import qismiga sudrab tashlash ham mumkin bo'ladi. Fayl tanlash maydoni qoladi — u klaviatura va ekran o'quvchi uchun asosiy yo'l, sudrash esa sichqoncha uchun qo'shimcha qulaylik.
1. Branch:
git switch -c feature/import-sudrash2. index.html — Import qismi zonaga aylanadi. Oldin sarlavha, textarea, tugma va fayl maydoni ota elementda yonma-yon turardi. Endi ular bitta <section> ichida — shu bo'lim butunlay tashlash zonasi:
<section id="import-zona" aria-labelledby="import-sarlavha">
<h2 id="import-sarlavha">Import</h2>
<p class="izoh">Eksport faylini shu yerga sudrab tashlash ham
mumkin.</p>
<label for="import-matn">Eksport qilingan JSON matni</label>
<textarea id="import-matn" rows="6"></textarea>
<button type="button" id="import-tugma">Import qilish</button>
<p>
<label for="import-fayl">Yoki eksport faylini tanlang</label>
<input type="file" id="import-fayl"
accept=".json,application/json">
</p>
<p id="import-xato" class="xato" aria-live="polite"></p>
</section>aria-labelledby — ekran o'quvchi bo'limni "Import, bo'lim" deb e'lon qiladi. .izoh matni sudrash imkoniyatini aytadi: zona ko'rinmas bo'lsa, foydalanuvchi u haqida bilmaydi. render.js ga: export const importZona = document.getElementById("import-zona");.
3. asosiy.js — zona tinglovchilari:
// Sudrab tashlash — sichqoncha uchun qulaylik; klaviatura va ekran
// o'quvchi uchun yo'l — yuqoridagi fayl tanlash maydoni
const faylmi = (event) => event.dataTransfer.types.includes("Files");
importZona.addEventListener("dragover", (event) => {
if (!faylmi(event)) {
return;
}
// shusiz brauzer bu yerga tashlashga yo'l bermaydi
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
importZona.classList.add("ustida");
});
importZona.addEventListener("dragleave", (event) => {
// Ichki elementga o'tish ham dragleave beradi — zonadan chiqmagan
if (!importZona.contains(event.relatedTarget)) {
importZona.classList.remove("ustida");
}
});
importZona.addEventListener("drop", (event) => {
if (!faylmi(event)) {
return;
}
event.preventDefault();
importZona.classList.remove("ustida");
const [fayl] = event.dataTransfer.files;
if (fayl !== undefined) {
faylniImportQil(fayl);
}
});Darsdagi «cheklarni tashlash» kodi bilan solishtiring — tuzilma bir xil: faylmi = hasFiles, ustida = over. Eng muhimi — oxirgi qator: faylniImportQil(fayl). Bu o'tgan darsdagi funksiya: nom va hajm tekshiruvi, fayl.text(), paketniOqi. Sudralgan fayl ham, tanlangan fayl ham bitta yo'ldan o'tadi — yangi tekshiruv kodi yozilmadi.
Zona textarea ni ham o'z ichiga oladi. Matnni textarea ga sudrash odatdagidek ishlaydi: faylmi false bo'lsa, preventDefault chaqirilmaydi va brauzer o'z ishini qiladi.
4. asosiy.js — oynadagi qo'riqchi:
// Zonadan tashqariga tashlangan faylni brauzer ochib yuboradi va
// ilova yopiladi — fayllar uchun buni to'xtatamiz (matnga tegmaymiz)
for (const tur of ["dragover", "drop"]) {
addEventListener(tur, (event) => {
if (faylmi(event)) {
event.preventDefault();
}
});
}addEventListener oldida window. yo'q — modulning yuqori darajasida u global window ga tegishli.
5. CSS (vazifalar.css):
#import-zona {
margin-inline: -0.5rem;
padding-inline: 0.5rem;
border: 2px dashed transparent;
border-radius: 0.5rem;
}
#import-zona.ustida {
border-color: var(--rang-asosiy);
background: var(--rang-yuza);
}
.izoh { margin: 0; color: var(--rang-ikkinchi); }Ramka doim bor, lekin shaffof — .ustida da faqat rangi o'zgaradi. Shunda zona "ustida" holatiga o'tganda sahifa 2 px ga sakramaydi. margin-inline: -0.5rem + padding-inline: 0.5rem — matn joyidan siljimasdan ramkaga joy ochiladi. Ranglar — mavjud tema o'zgaruvchilari, qorong'i temada ham to'g'ri.
6. Test. Mantiq o'zgarmadi (tekshiruv faylniTekshir da, u allaqachon testlangan) — 75/75; lint, format:check, tip — toza.
7. Brauzerda (Live Server). Kanon tekshiruvi sahifa ichida DataTransfer va File bilan DragEvent yasab sinadi:
✅ drop: import e'loni: "5 ta vazifa import qilindi."
✅ drop: noto'g'ri fayl — xato: [true,true,true]
✅ drop: xato matni: "Faqat .json fayl import qilinadi."
✅ drop: zonadan tashqarida — brauzer faylni ochmaydi: true
✅ drop: sahifa joyida: "/"Yana bitta qator zonaning "ustida" klassini tekshiradi: textarea ustida ham ruxsat berildi, ramka yondi va tashlangandan keyin o'chdi. O'zingiz: «Faylga yuklab olish» → bir vazifani o'chiring → yuklangan faylni Explorer'dan Import qismiga sudrang. Keyin faylni sahifaning boshqa joyiga tashlang — sahifa yopilmaydi.
8. Commit va PR:
git add index.html assets/css/vazifalar.css assets/js/asosiy.js \
assets/js/render.js
git commit -m \
"feat: eksport faylini Import qismiga sudrab tashlab import qilish"
git push -u origin feature/import-sudrash
gh pr create --fill
gh pr merge --mergeDiff: 4 fayl, +69 −10.
9. Real ishda
- Fayl yuklash. Gmail'ga ilova, Google Drive, GitHub'dagi izohga rasm, Telegram Web — hammasida fayl tashlash zonasi va yonida «Fayl tanlash».
- Kanban va tartiblash. Trello, Jira, Notion — kartalarni ustunlar orasida ko'chirish. Ular ko'pincha HTML DnD emas, Pointer Events asosidagi kutubxonalardan foydalanadi (SortableJS, React uchun dnd-kit; React — 17-qismdan, hozir bilish shart emas).
- Qulaylik (a11y) auditlari. "Sudrash bilan qilinadigan har ish tugma bilan ham qilinadimi?" — WCAG 2.2 dagi alohida talab, katta kompaniyalarda tekshiriladi.
- Intervyu. "
drophodisasi nega kelmayapti?", "dragleavenega miltillaydi?", "Mobil qurilmada sudrashni qanday qilasiz?" — frontend intervyularida uchraydi.
Xulosa
- Manba:
draggable="true"vadragstartdadataTransfer.setData(); nishon:dragoverdapreventDefault(),dropdagetData(). getData()faqatdropda ishlaydi;dragoverda qaror uchun —types.- Fayllar
dataTransfer.filesda; zonadan tashqarida brauzer faylni ochmasligi uchunwindowda qo'riqchi. dragleavemiltillashi —contains(event.relatedTarget)bilan.- HTML DnD telefonda ishonchsiz, klaviaturada ishlamaydi — fayl tanlash maydoni, tugmalar yoki Pointer Events muqobil.
- Tashlangan matnni
textContentbilan, havolani protokolini tekshirib ishlating.
Keyingi dars: Canvas 2D asoslari — <canvas> ichida JavaScript bilan chizish: shakllar, ranglar, matn va rasmlar.
Manbalar
- HTML Living Standard: "Drag and drop" — html.spec.whatwg.org
- MDN: HTML Drag and Drop API,
DataTransfer, "File drag and drop" — developer.mozilla.org - W3C: WCAG 2.2, 2.5.7 "Dragging Movements" — w3.org/TR/WCAG22
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!