UI hodisalari

Drag'n'Drop (sichqoncha bilan)

Drag'n'Drop (sudrab tashlash) — foydalanuvchiga qulay interfeys usuli. Faylni papkaga sudrash, savatga mahsulot tashlash, elementlar tartibini o'zgartirish — bularning barchasi shu texnikaga asoslanadi. Bu darsda sof sichqoncha hodisalari asosidagi Drag'n'Drop'ni chuqur ko'rib chiqamiz.

Brauzerda "tug'ma" Drag Events API ham mavjud (dragstart, dragend va h.k.). U fayllarni tashqi ilovalardan sudrab kirtish uchun qulay, lekin cheklovlari bor: gorizontal/vertikal harakatni cheklash yoki maxsus animatsiyalar qilish qiyin. Shuning uchun ko'p hollarda oddiy sichqoncha hodisalari afzal ko'riladi — biz shuni o'rganamiz.
Bu darsdagi kod misollari statik — DOM hodisalari playground'da ishlamaydi. Ularni haqiqiy HTML sahifada sinang.

Asosiy algoritm

Drag'n'Drop algoritmi juda sodda ko'rinadi — u uch hodisaga asoslanadi:

  1. mousedown — elementni bosganda sudrashni boshlaymiz.
  2. mousemove — kursor harakatlanganda elementni ko'chiramiz (uni position: absolute qilib, koordinatalarini yangilaymiz).
  3. mouseup — tugma qo'yib yuborilganda sudrashni to'xtatamiz.

Muhim nuqta: mousemoveni elementga emas, balki butun hujjatga (documentga) bog'lash kerak. Chunki kursor tez harakatlanganda element kursordan orqada qolishi va mousemove element ustida bo'lmasligi mumkin. Agar hodisani faqat elementga bog'lasak, sudrash "uzilib" qoladi.

let ball = document.getElementById('ball');

ball.onmousedown = function(event) {
  // Elementni body'ga to'g'ridan-to'g'ri joylashtiramiz
  ball.style.position = 'absolute';
  ball.style.zIndex = 1000;
  document.body.append(ball);

  function moveAt(pageX, pageY) {
    ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
    ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
  }

  // Elementni kursor markaziga joylashtiramiz
  moveAt(event.pageX, event.pageY);

  function onMouseMove(event) {
    moveAt(event.pageX, event.pageY);
  }

  // mousemove'ni HUJJATGA bog'laymiz
  document.addEventListener('mousemove', onMouseMove);

  // Qo'yib yuborilganda tozalaymiz
  ball.onmouseup = function() {
    document.removeEventListener('mousemove', onMouseMove);
    ball.onmouseup = null;
  };
};

Standart ondragstart harakatini o'chirish

Agar yuqoridagi kodni sinasangiz, tuval (canvas) yoki rasm (image) elementlarida g'alati holat kuzatiladi: sudrash o'rniga brauzer o'zining "native" drag-and-drop harakatini ishga tushiradi. Bu bizga xalaqit beradi.

Yechim — brauzerning standart dragstart harakatini bekor qilish:

ball.ondragstart = function() {
  return false;
};

Endi brauzer o'zining drag harakatini boshlamaydi va bizning kodimiz to'sqinliksiz ishlaydi.

Bu bitta qator ko'pincha unutiladi va "nega mening drag'n'drop'im ba'zi elementlarda buziladi?" degan savolga sabab bo'ladi. Rasm va havolalar (link) uchun bu ayniqsa muhim.

Sudrash markazini to'g'rilash

Yuqorida biz elementni kursor markaziga joylashtirdik. Lekin haqiqiy interfeyslarda foydalanuvchi elementning istalgan joyidan ushlashi mumkin. Agar shunda elementni har doim markazga ko'chirsak, u "sakrab" ketadi.

To'g'ri yechim: mousedown paytida kursor bilan element chekkasi orasidagi masofani (siljish, shift) hisoblab, keyingi harakatlarda uni saqlab qolish kerak:

ball.onmousedown = function(event) {
  // Kursor bilan element chap-yuqori burchagi orasidagi masofa
  let shiftX = event.clientX - ball.getBoundingClientRect().left;
  let shiftY = event.clientY - ball.getBoundingClientRect().top;

  function moveAt(pageX, pageY) {
    ball.style.left = pageX - shiftX + 'px';
    ball.style.top = pageY - shiftY + 'px';
  }
  // ...qolgan qismi yuqoridagidek
};
getBoundingClientRect() elementning oynaga nisbatan koordinatalarini beradi. Shu sabab bu yerda clientX/Y ishlatiladi (ikkalasi ham oynaga nisbatan). Keyin joylashtirishda esa hujjatga nisbatan pageX/Y va left/top (absolute) ishlatiladi.

pointer-events bilan nishonni aniqlash muammosi

Sudralayotgan element kursor tagida turadi. Bu bir muammo tug'diradi: kursor ostidagi haqiqiy nishon (masalan, savat yoki papka) qaysi element ekanini bilmoqchi bo'lsak, event.target har doim sudralayotgan elementning o'zini qaytaradi — chunki u kursor tagida yotibdi!

Yechim ikkita. Birinchisi — CSS pointer-events: none. Bu xossa elementni sichqoncha hodisalari uchun "ko'rinmas" qiladi — hodisalar undan o'tib, ostidagi elementga yetadi:

// Sudrayotgan element sichqoncha hodisalarini o'tkazib yuboradi:
// ball.style.pointerEvents = 'none';
// Endi event.target ostidagi haqiqiy elementni qaytaradi.
pointer-events: none oddiy va tez, lekin cheklovi bor: agar sudralayotgan elementda ham sichqoncha hodisalari kerak bo'lsa, bu ishlamaydi. Bunday holatda quyidagi elementFromPoint usuli afzal.

document.elementFromPoint bilan nishonni aniqlash

Ikkinchi, universal yechim — document.elementFromPoint(clientX, clientY) metodi. U berilgan oyna koordinatalaridagi eng ustki (topmost) elementni qaytaradi. Sudrash paytida sudralayotgan elementni vaqtincha yashirib, ostidagi nishonni topamiz:

// mousemove ichida:
function onMouseMove(event) {
  moveAt(event.pageX, event.pageY);

  // Sudralayotgan elementni yashiramiz
  ball.hidden = true;
  let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
  ball.hidden = false;

  // elemBelow null bo'lishi mumkin (kursor oyna tashqarisida bo'lsa)
  if (!elemBelow) return;

  // Nishon (droppable) elementni topamiz
  let droppableBelow = elemBelow.closest('.droppable');
  // ...droppableBelow ustida ekanmizmi, tekshiramiz
}
elementFromPoint(clientX, clientY) aynan oyna koordinatalarini (client), hujjat koordinatalarini emas (page) kutadi. Agar adashib pageX/pageY bersangiz va sahifa skroll qilingan bo'lsa — noto'g'ri element qaytadi. Shuningdek, agar koordinatalar oyna tashqarisida bo'lsa, metod null qaytaradi.
Sudralayotgan elementni ball.hidden = true bilan bir lahzaga yashirib, so'ng darhol ball.hidden = false qilish miltillash chaqirmaydi, chunki brauzer bu ikki o'zgarish orasida ekranni qayta chizmaydi (repaint).

Nishonlarni kuzatish: onDragEnter/onDragLeave

Yuqoridagi elemBelowni har mousemoveda hisoblab, oldingi qiymat bilan solishtirib, o'zimizning "nishonga kirdi / nishondan chiqdi" mantig'ini yozamiz. Bu bizga nishonni yoritish (highlight) yoki boshqa vizual javob berish imkonini beradi:

let currentDroppable = null;

function onMouseMove(event) {
  // ...moveAt va elemBelow ni hisoblaymiz...
  let droppableBelow = elemBelow ? elemBelow.closest('.droppable') : null;

  if (currentDroppable !== droppableBelow) {
    if (currentDroppable) {
      // Oldingi nishondan chiqdik
      leaveDroppable(currentDroppable);
    }
    currentDroppable = droppableBelow;
    if (currentDroppable) {
      // Yangi nishonga kirdik
      enterDroppable(currentDroppable);
    }
  }
}

Xulosa

Sichqoncha asosidagi Drag'n'Drop algoritmi:

  1. mousedown: sudrashni boshlash, shiftX/shiftY ni hisoblash, standart ondragstartni return false bilan o'chirish.
  2. mousemove (hujjatga bog'langan): elementni ko'chirish va elementFromPoint yoki pointer-events: none orqali nishonni aniqlash.
  3. mouseup: sudrashni to'xtatish va ishlov beruvchilarni tozalash (removeEventListener).
Chinakam kutubxonalar (masalan, Sortable.js, jQuery UI Draggable) shu asosda qurilgan, lekin ular tekislik (accessibility), sensorli qurilma va ko'plab chekka holatlarni ham hisobga oladi. Amaliy loyihalarda tayyor kutubxonadan foydalanish ko'pincha oqilona.