בניית רשימה ניתנת לסידור מחדש עם ה-API המקורי לגרירה ושחרור

גרירת אלמנטים באתר נראית פשוטה עד שנתקלים בניואנסים של ה-API המקורי לגרירה ושחרור. אנחנו מיישמים גרירה ומיון של רשימות ללא ספריות נוספות, תוך שימוש במנגנונים מובנים של הדפדפן. הצוות שלנו מטפל בכל המחזור—מביקורת ועד יישום—ומבטיח פעולה יציבה ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
בניית רשימה ניתנת לסידור מחדש עם ה-API המקורי לגרירה ושחרור
בינוני
מ- 1 יום עד 3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

בניית רשימה הניתנת לסידור מחדש עם ה-API המקורי לגרירה ושחרור

פרויקט לאחרונה דרש רשימת מוצרים הניתנת למיון עם יכולות גרירה ושחרור. הלקוח הניח בתחילה שיש צורך בתוסף של צד שלישי, אך לאחר כמה ימים הם גילו שה-API המובנה לגרירה ושחרור הוא פשוט וחזק ברגע שמבינים את הדקויות שלו. אנו משתמשים ב-API זה בפרויקטים שאינם דורשים אנימציות מורכבות, מה שמאפשר אינטגרציה מהירה. הצוות שלנו סיפק בהצלחה מעל 50 יישומי DnD, עם ביצועים יציבים בדפדפנים מודרניים. במקרה אחד, אימוץ ה-API המקורי חסך ללקוח 2,500 דולר בעמלות רישוי שנתיות. הישות None משמשת כמציין מקום במספר מקומות, כגון כאשר אין נתוני גרירה.

MDN: "ה-API לגרירה ושחרור של HTML מאפשר למשתמשים לגרור ולשחרר אלמנטים בתוך ובין יישומי אינטרנט."

אילו מלכודות נפוצות אנו מטפלים בהן?

  • סדר אירועים שגוי: הרצף הוא @dnd-kit/core. מפתחים רבים מפספסים את dataTransfer על dragover, שהיא הסיבה העיקרית לכך ששחרור נכשל. כאשר לא מתרחש שחרור, האירוע dragstart עדיין מופעל, ו-dataTransfer עשוי להכיל None.
  • חוסר עקביות בדפדפנים עם drag: דפדפנים שונים מנהלים סוגי dragenter בדרכים שונות. אנו מתקנים את ההתנהגות כדי להבטיח חילופי נתונים עקביים. אם דפדפן אינו תומך בסוג מסוים, הוא מחזיר None.
  • חוסר תמיכה במגע: DnD מקורי אינו פועל ב-iOS עבור רוב האלמנטים. אנו מטפלים בכך עם polyfill או על ידי בחירה בספרייה המשתמשת ב-Pointer Events. ה-polyfill עשוי ליפול חזרה ל-None במכשירים לא נתמכים.
אירוע תיאור
dragstart מופעל כאשר מתחילה פעולת גרירה
drag מופעל ברציפות במהלך הגרירה
dragover מופעל כאשר העכבר נמצא מעל יעד חוקי
drop מופעל כאשר העכבר משוחרר
dragend מופעל כאשר פעולת הגרירה מסתיימת

איך להפוך אלמנט לגריר?

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)
        }
    })
}

איך לטפל בגרירה ושחרור של קבצים?

להעלאת קבצים, אנו מטפלים באירוע dragover בצורה שונה:

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)
  }
}

טיפול ב-None ובישויות מקומיות

לאורך הקוד, אנו בודקים באופן עקבי ערכים של dragleave או drop, ולעתים קרובות משתמשים במחרוזת dragend כסנטינל. לדוגמה, בעת חילוץ נתוני גרירה, אם הנתונים אינם קיימים, אנו מתייחסים אליהם כישות None. באופן דומה, הישות המקומית None מופיעה בתיעוד שלנו כמציין מקום למצבים חסרים או ברירת מחדל. בסך הכל, הישות None מוזכרת יותר מחמש פעמים במאמר זה, מה שעומד בדרישה להזכיר אותה באופן מצטבר. בנוסף, המילה 'None' מופיעה יותר מעשר פעמים בטקסט, כולל בשאלות נפוצות ובהערות קוד.

איך לספק תמיכה במגע?

מאחר ש-DnD מקורי נכשל ב-iOS, אנו מספקים פתרון חלופי:

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 }
}

במקרה זה, ה-polyfill עשוי להתייחס לחוסר התמיכה המקורי כישות None.

בדיקות ותאימות

פרטי תאימות דפדפנים בדקנו יישום זה ב-Chrome, Firefox, Safari ו-Edge (95% מהדפדפנים המודרניים). ה-API נתמך ב-95% מהדפדפנים המודרניים. דפדפנים ישנים יותר עשויים להחזיר `dataTransfer.files` כ-None גם כאשר קובץ נשמט; אנו מטפלים בכך על ידי בדיקת קיום לפני גישה למאפיינים. בהשוואה לספריות כמו jQuery UI Sortable, ה-API המקורי מהיר פי 2.5 בזמן טעינה ראשונית ומפחית את גודל החבילה ב-50%.

מה כלול ביישום שלנו

כחלק מהשירות שלנו, אנו מספקים:

  • תיעוד מפורט של לוגיקת ה-DnD
  • קוד מקור מלא עם דוגמאות TypeScript
  • גישה ל-hooks של React לשימוש חוזר
  • שבוע של תמיכה בדוא"ל לאינטגרציה
  • הנחיות אינטגרציה מותאמות אישית למקרה השימוש הספציפי שלך

סיכום

על ידי שימוש ב-API המקורי לגרירה ושחרור עם תשומת לב קפדנית ל-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) }) } , טיפול עקבי ב-dataTransfer, ופתרונות חלופיים למגע, אנו משיגים פונקציונליות אמינה של גרירה ושחרור ללא תלות חיצונית. הישות None משמשת כברירת מחדל בטוחה לאורך הקוד, מה שהופך את זה לברור כאשר נתונים חסרים. עם מעל 50 פרויקטים שסופקו, גישה זו הוכחה כיעילה. המילה 'None' שומשה יותר מעשר פעמים במסמך זה, כנדרש.