Реалізація Drag-and-Drop API на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Drag-and-Drop API на сайті
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Впровадження Drag-and-Drop API на веб-сайті

Нативний Drag and Drop API браузера — не найприємніший інтерфейс для роботи. Порядок подій неочевидний, dataTransfer поводиться по-різному в різних браузерах, а dragover потребує попередження за замовчуванням, інакше drop не спрацює. Однак він вбудований у браузер, підтримує перетягування файлів з ОС, працює без бібліотек і не навантажує bundle.

Для складних сортованих списків з підтримкою дотику та анімацій — варто поглянути на @dnd-kit/core. Для завантаження файлів і базового перетягування — нативного API достатньо.

Основні事件

Порядок подій при перетягуванні:

dragstart → dragenter → dragover (кожні ~50ms) → drop → dragend

На елементі-джерелі: dragstart, drag, dragend. На елементі-цілі: dragenter, dragover, dragleave, drop.

Перетягуваний елемент

function makeDraggable(element: HTMLElement, data: Record<string, string>): void {
  element.setAttribute('draggable', 'true')

  element.addEventListener('dragstart', (event: DragEvent) => {
    if (!event.dataTransfer) return

    // Встановлюємо дані для передачі
    for (const [type, value] of Object.entries(data)) {
      event.dataTransfer.setData(type, value)
    }

    // Тип операції: copy | move | link
    event.dataTransfer.effectAllowed = 'move'

    // Користувацький drag-ghost
    const ghost = element.cloneNode(true) as HTMLElement
    ghost.style.cssText = 'position:absolute;top:-9999px;opacity:0.8'
    document.body.appendChild(ghost)
    event.dataTransfer.setDragImage(ghost, 0, 0)
    setTimeout(() => document.body.removeChild(ghost), 0)

    element.classList.add('is-dragging')
  })

  element.addEventListener('dragend', () => {
    element.classList.remove('is-dragging')
  })
}

Зона скидання

function makeDropZone(
  zone: HTMLElement,
  onDrop: (data: string, event: DragEvent) => void,
  acceptType = 'text/plain'
): void {
  // Без preventDefault() тут drop не спрацює
  zone.addEventListener('dragover', (event: DragEvent) => {
    if (!event.dataTransfer?.types.includes(acceptType)) return
    event.preventDefault()
    event.dataTransfer.dropEffect = 'move'
    zone.classList.add('drop-zone--active')
  })

  zone.addEventListener('dragleave', (event: DragEvent) => {
    // Перевіримо, що курсор дійсно покинув зону (не увійшов у дочірній елемент)
    if (!zone.contains(event.relatedTarget as Node)) {
      zone.classList.remove('drop-zone--active')
    }
  })

  zone.addEventListener('drop', (event: DragEvent) => {
    event.preventDefault()
    zone.classList.remove('drop-zone--active')
    const data = event.dataTransfer?.getData(acceptType)
    if (data) onDrop(data, event)
  })
}

Завантаження файлів через перетягування

function makeFileDropZone(
  zone: HTMLElement,
  onFiles: (files: FileList) => void,
  accept?: string[]
): void {
  zone.addEventListener('dragover', (event: DragEvent) => {
    if (!event.dataTransfer?.types.includes('Files')) return
    event.preventDefault()
    event.dataTransfer.dropEffect = 'copy'
    zone.classList.add('drop-zone--active')
  })

  zone.addEventListener('dragleave', (event: DragEvent) => {
    if (!zone.contains(event.relatedTarget as Node)) {
      zone.classList.remove('drop-zone--active')
    }
  })

  zone.addEventListener('drop', (event: DragEvent) => {
    event.preventDefault()
    zone.classList.remove('drop-zone--active')

    const files = event.dataTransfer?.files
    if (!files?.length) return

    if (accept) {
      const filtered = Array.from(files).filter((f) =>
        accept.some((type) =>
          type.startsWith('.') ? f.name.endsWith(type) : f.type.startsWith(type.replace('*', ''))
        )
      )
      if (!filtered.length) return
      const dt = new DataTransfer()
      filtered.forEach((f) => dt.items.add(f))
      onFiles(dt.files)
    } else {
      onFiles(files)
    }
  })
}

Сортуваний список

Класичний паттерн — перетягування карток для змінення порядку:

interface SortableItem {
  id: string
  element: HTMLElement
}

class SortableList {
  private items: SortableItem[] = []
  private draggedId: string | null = null

  constructor(
    private container: HTMLElement,
    private onChange: (ids: string[]) => void
  ) {}

  add(id: string, element: HTMLElement): void {
    element.setAttribute('draggable', 'true')
    element.dataset.id = id

    element.addEventListener('dragstart', (e: DragEvent) => {
      this.draggedId = id
      e.dataTransfer!.setData('text/plain', id)
      e.dataTransfer!.effectAllowed = 'move'
      element.classList.add('sortable--dragging')
    })

    element.addEventListener('dragend', () => {
      element.classList.remove('sortable--dragging')
      this.draggedId = null
      this.container.querySelectorAll('.sortable--over').forEach((el) =>
        el.classList.remove('sortable--over')
      )
    })

    element.addEventListener('dragover', (e: DragEvent) => {
      e.preventDefault()
      if (this.draggedId === id) return
      element.classList.add('sortable--over')

      // Вставити перетягуваний елемент перед поточним
      const draggedEl = this.container.querySelector(`[data-id="${this.draggedId}"]`)
      if (draggedEl && draggedEl !== element) {
        const rect = element.getBoundingClientRect()
        const insertBefore = e.clientY < rect.top + rect.height / 2
        element.parentNode?.insertBefore(
          draggedEl,
          insertBefore ? element : element.nextSibling
        )
      }
    })

    element.addEventListener('dragleave', () => {
      element.classList.remove('sortable--over')
    })

    element.addEventListener('drop', (e: DragEvent) => {
      e.preventDefault()
      element.classList.remove('sortable--over')
      // Порядок уже оновлено у dragover, тут повідомляємо зовні
      const newOrder = Array.from(
        this.container.querySelectorAll('[data-id]')
      ).map((el) => (el as HTMLElement).dataset.id!)
      this.onChange(newOrder)
    })

    this.items.push({ id, element })
    this.container.appendChild(element)
  }
}

React Hook для перетягування та скидання

function useDraggable(id: string) {
  const [isDragging, setIsDragging] = useState(false)

  const dragHandlers = {
    draggable: true as const,
    onDragStart: (e: React.DragEvent) => {
      e.dataTransfer.setData('text/plain', id)
      e.dataTransfer.effectAllowed = 'move'
      setIsDragging(true)
    },
    onDragEnd: () => setIsDragging(false),
  }

  return { isDragging, dragHandlers }
}

function useDroppable(onDrop: (id: string) => void) {
  const [isOver, setIsOver] = useState(false)

  const dropHandlers = {
    onDragOver: (e: React.DragEvent) => {
      e.preventDefault()
      setIsOver(true)
    },
    onDragLeave: (e: React.DragEvent) => {
      if (!(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) {
        setIsOver(false)
      }
    },
    onDrop: (e: React.DragEvent) => {
      e.preventDefault()
      setIsOver(false)
      const id = e.dataTransfer.getData('text/plain')
      if (id) onDrop(id)
    },
  }

  return { isOver, dropHandlers }
}

Сенсорні пристрої

Нативний DnD на iOS не працює на більшості елементів. Для підтримки дотику потрібен поліфіл (drag-touch) або бібліотека @dnd-kit/core, яка використовує Pointer Events API і працює на всіх пристроях. Вибір залежить від вимог проекту.

Що включено

Впровадження перетягуваних елементів і зон скидання, сортуваний список (якщо потрібно), завантаження файлів через перетягування з фільтруванням за типом, React хуки, CSS стилі для станів перетягування, розв'язання питання підтримки дотику.

Терміни: 1–2 дні залежно від складності сценаріїв та необхідності підтримки дотику.