Now.js Framework Documentation

Now.js Framework Documentation

Sortable - คอมโพเนนต์ลากและวาง

TH 01 Dec 2025 01:04

Sortable - คอมโพเนนต์ลากและวาง

เอกสารฉบับนี้อธิบาย Sortable ซึ่งเป็นคอมโพเนนต์สำหรับการลากและวางเพื่อจัดเรียงรายการ รองรับการลากระหว่างคอนเทนเนอร์ และสามารถบันทึกข้อมูลผ่าน API อัตโนมัติได้

📋 สารบัญ

  1. ภาพรวม
  2. การติดตั้งและนำเข้า
  3. การใช้งานพื้นฐาน
  4. คุณสมบัติ HTML
  5. ตัวเลือกการกำหนดค่า
  6. การลากด้วยปุ่มจับ vs พื้นที่ทั้งหมด
  7. การลากข้ามคอนเทนเนอร์
  8. การบันทึกผ่าน API
  9. การทำงานร่วมกับ FileElementFactory
  10. การทำงานร่วมกับ ApiComponent
  11. อีเวนต์
  12. JavaScript API
  13. ตัวอย่างการใช้งาน
  14. แนวทางปฏิบัติที่ดี

ภาพรวม

Sortable เป็นคอมโพเนนต์ที่ช่วยให้สามารถลากและวางรายการเพื่อจัดเรียงลำดับได้ รองรับทั้งการใช้งานผ่าน HTML attributes และ JavaScript API

ฟีเจอร์หลัก

  • ลากและวางที่ลื่นไหล: ใช้งานง่าย มีแอนิเมชั่นที่สวยงาม
  • ปุ่มจับแบบกำหนดเอง: เลือกได้ว่าจะลากจากปุ่มจับหรือพื้นที่ทั้งหมด
  • ลากข้ามคอนเทนเนอร์: ย้ายรายการระหว่างกลุ่มได้ (เช่น Kanban Board)
  • บันทึกอัตโนมัติ: เชื่อมต่อ API เพื่อบันทึกการเปลี่ยนแปลงทันที
  • รองรับทัชสกรีน: ใช้งานได้ทั้งเมาส์และทัช
  • รองรับคีย์บอร์ด: ใช้ลูกศรและ Space bar ในการจัดเรียง
  • ระบบอีเวนต์: ตรวจจับการเปลี่ยนแปลงและดำเนินการตามต้องการ
  • ทำงานร่วมกับ FileElementFactory: จัดเรียงไฟล์ได้อย่างง่ายดาย

เมื่อไหร่ควรใช้ Sortable

ใช้ Sortable เมื่อ:

  • ต้องการให้ผู้ใช้จัดเรียงรายการด้วยการลากและวาง
  • สร้าง Kanban Board หรือ Task Board
  • จัดเรียงไฟล์ที่อัปโหลด
  • สร้างรายการที่สามารถจัดลำดับความสำคัญได้
  • ต้องการบันทึกลำดับผ่าน API อัตโนมัติ

ไม่ควรใช้ Sortable เมื่อ:

  • รายการมีจำนวนมากเกินไป (มากกว่า 1000 รายการ - ควรใช้ virtual scrolling)
  • ต้องการการลากและวางที่ซับซ้อนมาก (พิจารณาใช้ library เฉพาะทาง)
  • ไม่ต้องการให้ผู้ใช้เปลี่ยนลำดับได้

การติดตั้งและนำเข้า

Sortable โหลดมาพร้อมกับ Now.js Framework และพร้อมใช้งานทันทีผ่าน window object:

// ไม่ต้อง import - พร้อมใช้งานทันที
console.log(window.Sortable); // คลาส Sortable

สิ่งที่ต้องพึ่งพา

  • ErrorManager – สำหรับจัดการข้อผิดพลาด (ทางเลือก)
  • NotificationManager – สำหรับแสดงการแจ้งเตือน (ทางเลือก สำหรับ API integration)

การใช้งานพื้นฐาน

1. การใช้งานผ่าน HTML (แนะนำ)

<!-- รายการที่สามารถลากและวางได้ -->
<div data-component="sortable">
  <div draggable="true" data-id="1">รายการ 1</div>
  <div draggable="true" data-id="2">รายการ 2</div>
  <div draggable="true" data-id="3">รายการ 3</div>
</div>

อธิบาย:

  • data-component="sortable" - กำหนดให้เป็น Sortable component
  • draggable="true" - ทำให้รายการสามารถลากได้
  • data-id - ID ของรายการ (ใช้สำหรับ API integration)

2. การใช้งานผ่าน JavaScript

const container = document.querySelector('#my-list');

const sortable = new Sortable(container, {
  draggable: '.item',
  animation: 200,
  onEnd: (evt) => {
    console.log('ย้ายจาก', evt.oldIndex, 'ไปที่', evt.newIndex);
  }
});

3. ตัวอย่างพื้นฐานที่สมบูรณ์

<!DOCTYPE html>
<html>
<head>
  <style>
    .sortable-list {
      list-style: none;
      padding: 0;
    }

    .sortable-item {
      padding: 15px;
      margin: 5px 0;
      background: #f5f5f5;
      border: 1px solid #ddd;
      border-radius: 4px;
      cursor: move;
    }

    .sortable-ghost {
      opacity: 0.4;
      background: #c8ebfb;
    }

    .sortable-drag {
      opacity: 0.8;
    }
  </style>
</head>
<body>
  <ul class="sortable-list" data-component="sortable">
    <li class="sortable-item" draggable="true" data-id="1">
      📝 งาน 1: ออกแบบ UI
    </li>
    <li class="sortable-item" draggable="true" data-id="2">
      📝 งาน 2: พัฒนา Backend
    </li>
    <li class="sortable-item" draggable="true" data-id="3">
      📝 งาน 3: ทดสอบระบบ
    </li>
  </ul>

  <script src="Now/Now.js"></script>
</body>
</html>

คุณสมบัติ HTML

Sortable รองรับ attributes เหล่านี้สำหรับการกำหนดค่า:

คุณสมบัติพื้นฐาน

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-component string - ต้องเป็น "sortable"
data-draggable string '[draggable="true"]' Selector สำหรับรายการที่ลากได้
data-handle string null Selector สำหรับปุ่มจับ (ถ้าไม่ระบุ ลากได้ทั้งรายการ)
data-animation number 150 ระยะเวลาแอนิเมชั่น (มิลลิวินาที)
data-ghost-class string 'sortable-ghost' CSS class สำหรับ ghost element
data-drag-class string 'sortable-drag' CSS class สำหรับรายการที่กำลังลาก

คุณสมบัติการลากข้ามคอนเทนเนอร์

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-group string null ชื่อกลุ่มสำหรับการลากข้ามคอนเทนเนอร์

คุณสมบัติ API Integration

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-sortable-api string - URL ของ API endpoint
data-sortable-method string 'PUT' HTTP method (GET, POST, PUT, DELETE)
data-sortable-id-attr string 'data-id' Attribute ที่เก็บ ID ของรายการ
data-sortable-stage-attr string 'data-category' Attribute ที่เก็บหมวดหมู่/สถานะของคอนเทนเนอร์
data-sortable-update-field string 'category' ชื่อ field ที่จะส่งใน API payload
data-sortable-extra-data JSON null ข้อมูลเพิ่มเติมที่จะส่งไปกับ API

ตัวอย่างการใช้ Attributes

<div data-component="sortable"
     data-draggable=".task-card"
     data-handle=".drag-handle"
     data-animation="200"
     data-group="tasks"
     data-sortable-api="/api/tasks/update"
     data-sortable-id-attr="data-task-id"
     data-sortable-stage-attr="data-status"
     data-sortable-update-field="status">
  <!-- รายการที่นี่ -->
</div>

ตัวเลือกการกำหนดค่า

เมื่อสร้าง instance ด้วย JavaScript สามารถกำหนดค่าได้ทั้งหมด:

const sortable = new Sortable(element, {
  // การตั้งค่าพื้นฐาน
  draggable: '[draggable="true"]',  // Selector สำหรับรายการที่ลากได้
  handle: null,                      // Selector สำหรับปุ่มจับ
  animation: 150,                    // ระยะเวลาแอนิเมชั่น (ms)
  ghostClass: 'sortable-ghost',      // CSS class สำหรับ ghost
  dragClass: 'sortable-drag',        // CSS class สำหรับรายการที่ลาก
  dataIdAttr: 'data-id',            // Attribute สำหรับ ID

  // การตั้งค่าพฤติกรรม
  forceFallback: false,              // บังคับใช้ fallback mode
  fallbackTolerance: 0,              // ระยะทางก่อนเริ่มลาก (px)
  scroll: true,                      // เปิดใช้ auto-scroll
  scrollSensitivity: 30,             // ระยะจากขอบที่จะเริ่ม scroll (px)
  scrollSpeed: 10,                   // ความเร็วในการ scroll
  rtl: false,                        // โหมด Right-to-Left
  disabled: false,                   // ปิดการใช้งาน

  // การลากข้ามคอนเทนเนอร์
  group: null,                       // ชื่อกลุ่ม

  // API Integration
  apiEndpoint: '/api/items/update',  // API endpoint
  apiMethod: 'PUT',                  // HTTP method
  apiIdAttr: 'data-id',             // Attribute สำหรับ ID
  apiStageAttr: 'data-category',    // Attribute สำหรับหมวดหมู่
  apiUpdateField: 'category',        // Field ที่จะส่งใน payload
  apiExtraData: null,                // ข้อมูลเพิ่มเติม

  // Callbacks
  onStart: (evt) => {
    // เรียกเมื่อเริ่มลาก
    console.log('เริ่มลาก:', evt.item);
  },

  onEnd: (evt) => {
    // เรียกเมื่อวางรายการ
    console.log('วางแล้ว:', evt.item);
    console.log('จาก:', evt.oldIndex, 'ไป:', evt.newIndex);
  }
});

การลากด้วยปุ่มจับ vs พื้นที่ทั้งหมด

Sortable รองรับ 2 โหมดการลาก:

1. ลากจากพื้นที่ทั้งหมด (ค่าเริ่มต้น)

<!-- ลากได้จากทุกส่วนของรายการ -->
<div data-component="sortable">
  <div draggable="true" class="item">
    <h3>หัวข้อ</h3>
    <p>รายละเอียด</p>
  </div>
</div>

2. ลากจากปุ่มจับเท่านั้น

<!-- ลากได้จากปุ่มจับเท่านั้น -->
<div data-component="sortable" data-handle=".drag-handle">
  <div draggable="true" class="item">
    <span class="drag-handle">⋮⋮</span>
    <h3>หัวข้อ</h3>
    <p>รายละเอียด</p>
  </div>
</div>

<style>
.drag-handle {
  cursor: move;
  padding: 10px;
  color: #999;
  font-size: 20px;
}

.item {
  display: flex;
  align-items: center;
  gap: 10px;
}
</style>

เมื่อไหร่ควรใช้แบบไหน

โหมด ข้อดี ข้อเสีย เหมาะกับ
พื้นที่ทั้งหมด ใช้งานง่าย, ไม่ต้องมีปุ่มพิเศษ อาจลากโดยไม่ตั้งใจ รายการง่ายๆ, ไม่มีปุ่มอื่น
ปุ่มจับ ควบคุมได้ดี, ไม่ลากโดยไม่ตั้งใจ ต้องออกแบบ UI เพิ่ม รายการที่มีปุ่มหลายอัน, การ์ดที่ซับซ้อน

การลากข้ามคอนเทนเนอร์

Sortable รองรับการลากรายการระหว่างคอนเทนเนอร์ต่างๆ โดยใช้ group

ตัวอย่าง: Kanban Board

<div class="kanban-board">
  <!-- คอลัมน์ To Do -->
  <div class="kanban-column"
       data-component="sortable"
       data-group="tasks"
       data-category="todo"
       data-sortable-api="/api/tasks/update"
       data-sortable-stage-attr="data-category">
    <h3>To Do</h3>
    <div class="task-card" draggable="true" data-id="1">
      <h4>ออกแบบ UI</h4>
      <p>สร้าง mockup หน้าหลัก</p>
    </div>
    <div class="task-card" draggable="true" data-id="2">
      <h4>เขียนเอกสาร</h4>
      <p>เอกสาร API</p>
    </div>
  </div>

  <!-- คอลัมน์ In Progress -->
  <div class="kanban-column"
       data-component="sortable"
       data-group="tasks"
       data-category="in-progress"
       data-sortable-api="/api/tasks/update"
       data-sortable-stage-attr="data-category">
    <h3>In Progress</h3>
    <div class="task-card" draggable="true" data-id="3">
      <h4>พัฒนา Backend</h4>
      <p>สร้าง REST API</p>
    </div>
  </div>

  <!-- คอลัมน์ Done -->
  <div class="kanban-column"
       data-component="sortable"
       data-group="tasks"
       data-category="done"
       data-sortable-api="/api/tasks/update"
       data-sortable-stage-attr="data-category">
    <h3>Done</h3>
    <div class="task-card" draggable="true" data-id="4">
      <h4>ติดตั้งเซิร์ฟเวอร์</h4>
      <p>ติดตั้ง Ubuntu + Nginx</p>
    </div>
  </div>
</div>

<style>
.kanban-board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

.kanban-column {
  background: #f5f5f5;
  padding: 15px;
  border-radius: 8px;
  min-height: 400px;
}

.task-card {
  background: white;
  padding: 15px;
  margin: 10px 0;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  cursor: move;
}

.task-card h4 {
  margin: 0 0 5px 0;
}

.task-card p {
  margin: 0;
  color: #666;
  font-size: 14px;
}
</style>

อธิบาย:

  • data-group="tasks" - คอนเทนเนอร์ทั้ง 3 อยู่ในกลุ่มเดียวกัน สามารถลากข้ามกันได้
  • data-category - แต่ละคอลัมน์มีหมวดหมู่ต่างกัน
  • เมื่อลากการ์ดไปคอลัมน์อื่น จะส่ง API request อัตโนมัติ

การบันทึกผ่าน API

Sortable สามารถบันทึกการเปลี่ยนแปลงผ่าน API อัตโนมัติเมื่อลากรายการไปคอนเทนเนอร์อื่น

การตั้งค่า API Integration

<div data-component="sortable"
     data-sortable-api="/api/items/update"
     data-sortable-method="PUT"
     data-sortable-id-attr="data-id"
     data-sortable-stage-attr="data-status"
     data-sortable-update-field="status">
  <!-- รายการ -->
</div>

Request ที่จะส่งไป

เมื่อลากรายการไปคอนเทนเนอร์อื่น Sortable จะส่ง request:

// PUT /api/items/update
{
  "id": "123",              // จาก data-id ของรายการ
  "status": "in-progress",  // จาก data-status ของคอนเทนเนอร์ปลายทาง
  "update_stage_only": true // flag สำหรับการอัปเดตเฉพาะสถานะ
}

Response ที่คาดหวัง

{
  "success": true,
  "message": "อัปเดตสำเร็จ",
  "data": {
    "id": "123",
    "status": "in-progress"
  }
}

ตัวอย่าง API Endpoint (PHP)

<?php
// api/items/update.php

header('Content-Type: application/json');

$data = json_decode(file_get_contents('php://input'), true);

$id = $data['id'] ?? null;
$status = $data['status'] ?? null;

if (!$id || !$status) {
    echo json_encode([
        'success' => false,
        'message' => 'ข้อมูลไม่ครบถ้วน'
    ]);
    exit;
}

// อัปเดตฐานข้อมูล
$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$stmt = $db->prepare('UPDATE items SET status = ? WHERE id = ?');
$stmt->execute([$status, $id]);

echo json_encode([
    'success' => true,
    'message' => 'อัปเดตสำเร็จ',
    'data' => [
        'id' => $id,
        'status' => $status
    ]
]);

การส่งข้อมูลเพิ่มเติม

<div data-component="sortable"
     data-sortable-api="/api/items/update"
     data-sortable-extra-data='{"project_id": 42, "user_id": 123}'>
  <!-- ข้อมูลเพิ่มเติมจะถูกส่งไปด้วยทุกครั้ง -->
</div>

การทำงานร่วมกับ FileElementFactory

Sortable ทำงานร่วมกับ FileElementFactory ได้อย่างลงตัว สำหรับการจัดเรียงไฟล์ที่อัปโหลด

ตัวอย่างการใช้งาน

<form>
  <div class="form-group">
    <label>อัปโหลดรูปภาพ</label>
    <input type="file"
           id="product-images"
           data-element="file"
           data-preview="true"
           data-sortable="true"
           data-action-url="/api/products/images"
           data-file-reference="id"
           data-files='[
             {"id": 1, "url": "/uploads/image1.jpg", "name": "image1.jpg"},
             {"id": 2, "url": "/uploads/image2.jpg", "name": "image2.jpg"}
           ]'
           multiple>
  </div>
</form>

อธิบาย:

  • data-sortable="true" - เปิดใช้งาน Sortable สำหรับไฟล์
  • data-action-url - API endpoint สำหรับบันทึกลำดับ
  • data-file-reference="id" - ใช้ field id เป็น reference
  • FileElementFactory จะสร้าง Sortable instance อัตโนมัติ

API Request เมื่อจัดเรียงไฟล์

// POST /api/products/images
{
  "action": "sort",
  "order": [
    {"id": 2, "position": 0},
    {"id": 1, "position": 1}
  ]
}

การกำหนดค่า Sortable ผ่าน FileElementFactory

FileElementFactory จะสร้าง Sortable ด้วยการตั้งค่าเหล่านี้:

new Sortable(previewContainer, {
  animation: 150,
  handle: '.drag-handle',        // ลากจากปุ่มจับเท่านั้น
  draggable: '.preview-item',    // รายการที่ลากได้
  ghostClass: 'sortable-ghost',
  chosenClass: 'sortable-chosen',
  dragClass: 'sortable-drag',
  onEnd: async (evt) => {
    // บันทึกลำดับใหม่ผ่าน API
  }
});

การทำงานร่วมกับ ApiComponent

Sortable สามารถทำงานร่วมกับ ApiComponent เพื่อโหลดข้อมูลและจัดการการอัปเดต

ตัวอย่าง: Task List ที่โหลดจาก API

<!-- โหลดรายการจาก API -->
<div data-component="api"
     data-endpoint="/api/tasks"
     data-params='{"status": "active"}'>

  <template>
    <!-- Sortable container -->
    <div data-component="sortable"
         data-group="tasks"
         data-sortable-api="/api/tasks/update"
         data-sortable-id-attr="data-task-id"
         data-sortable-stage-attr="data-priority"
         data-sortable-update-field="priority">

      <!-- Loop รายการ -->
      <div data-for="task in data"
           class="task-item"
           draggable="true"
           data-bind:data-task-id="task.id"
           data-bind:data-priority="task.priority">
        <span class="drag-handle">⋮⋮</span>
        <span data-bind="task.title"></span>
      </div>
    </div>
  </template>
</div>

ตัวอย่าง: รีเฟรชหลังจากอัปเดต

// ฟังอีเวนต์จาก Sortable
document.addEventListener('sortable:end', (e) => {
  const sortableElement = e.target;

  // หา ApiComponent ที่เกี่ยวข้อง
  const apiElement = sortableElement.closest('[data-component="api"]');

  if (apiElement && apiElement.apiInstance) {
    // รีเฟรชข้อมูลหลังจากจัดเรียงเสร็จ
    setTimeout(() => {
      apiElement.apiInstance.refresh();
    }, 500);
  }
});

อีเวนต์

Sortable ส่งอีเวนต์เมื่อมีการเปลี่ยนแปลง

ประเภทของอีเวนต์

อีเวนต์ เมื่อไหร่ รายละเอียด
sortable:start เริ่มลาก {item, startIndex, event}
sortable:end วางรายการ {item, newIndex, oldIndex, to, from}
sortable:change ตำแหน่งเปลี่ยน {item, newIndex, oldIndex} หรือ {item, to, from}
sortable:select เลือกรายการด้วย Space {item, selected}
sortable:api-success API สำเร็จ {item, response, payload}
sortable:api-error API ล้มเหลว {item, error}

การฟังอีเวนต์

const container = document.querySelector('#my-sortable');

// เริ่มลาก
container.addEventListener('sortable:start', (e) => {
  console.log('เริ่มลาก:', e.detail.item);
  console.log('จากตำแหน่ง:', e.detail.startIndex);
});

// วางรายการ
container.addEventListener('sortable:end', (e) => {
  console.log('วางแล้ว:', e.detail.item);
  console.log('จาก:', e.detail.oldIndex, 'ไป:', e.detail.newIndex);
  console.log('คอนเทนเนอร์เดิม:', e.detail.from);
  console.log('คอนเทนเนอร์ใหม่:', e.detail.to);
});

// ตำแหน่งเปลี่ยน
container.addEventListener('sortable:change', (e) => {
  console.log('ตำแหน่งเปลี่ยน:', e.detail);
});

// API สำเร็จ
container.addEventListener('sortable:api-success', (e) => {
  console.log('บันทึกสำเร็จ:', e.detail.response);
  NotificationManager.success('บันทึกลำดับเรียบร้อย');
});

// API ล้มเหลว
container.addEventListener('sortable:api-error', (e) => {
  console.error('บันทึกล้มเหลว:', e.detail.error);
  NotificationManager.error('ไม่สามารถบันทึกลำดับได้');
});

JavaScript API

Sortable มี methods สำหรับควบคุมผ่าน JavaScript

สร้าง Instance

const sortable = new Sortable(element, options);

เข้าถึง Instance

// จาก element โดยตรง
const sortable = element._sortableInstance;

// หรือจาก ComponentManager
const sortable = ComponentManager.getInstance(element, 'sortable');

Methods

enable()

เปิดใช้งาน Sortable

sortable.enable();

disable()

ปิดใช้งาน Sortable

sortable.disable();

destroy()

ทำลาย instance และลบ event listeners

sortable.destroy();

ตัวอย่างการใช้งาน

1. รายการง่ายๆ

<ul data-component="sortable" class="simple-list">
  <li draggable="true" data-id="1">🍎 แอปเปิล</li>
  <li draggable="true" data-id="2">🍌 กล้วย</li>
  <li draggable="true" data-id="3">🍊 ส้ม</li>
  <li draggable="true" data-id="4">🍇 องุ่น</li>
</ul>

<style>
.simple-list {
  list-style: none;
  padding: 0;
}

.simple-list li {
  padding: 10px;
  margin: 5px 0;
  background: #f0f0f0;
  border-radius: 4px;
  cursor: move;
}

.simple-list li.sortable-ghost {
  opacity: 0.4;
}
</style>

2. การ์ดพร้อมปุ่มจับ

<div data-component="sortable"
     data-handle=".drag-handle"
     class="card-list">

  <div class="card" draggable="true" data-id="1">
    <span class="drag-handle">⋮⋮</span>
    <div class="card-content">
      <h3>บทความ 1</h3>
      <p>เนื้อหาบทความ...</p>
      <button>แก้ไข</button>
      <button>ลบ</button>
    </div>
  </div>

  <div class="card" draggable="true" data-id="2">
    <span class="drag-handle">⋮⋮</span>
    <div class="card-content">
      <h3>บทความ 2</h3>
      <p>เนื้อหาบทความ...</p>
      <button>แก้ไข</button>
      <button>ลบ</button>
    </div>
  </div>
</div>

<style>
.card {
  display: flex;
  gap: 10px;
  padding: 15px;
  margin: 10px 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.drag-handle {
  cursor: move;
  color: #999;
  font-size: 20px;
  padding: 5px;
}

.card-content {
  flex: 1;
}

.card button {
  margin-right: 5px;
}
</style>

3. Kanban Board แบบสมบูรณ์

<div class="kanban-board">
  <div class="kanban-column"
       data-component="sortable"
       data-group="kanban"
       data-category="todo"
       data-sortable-api="/api/tasks/update"
       data-sortable-stage-attr="data-category">
    <h3>📋 To Do</h3>
    <div class="kanban-card" draggable="true" data-id="1">
      <h4>ออกแบบ Logo</h4>
      <p>สร้าง logo ใหม่สำหรับบริษัท</p>
      <span class="badge">Design</span>
    </div>
  </div>

  <div class="kanban-column"
       data-component="sortable"
       data-group="kanban"
       data-category="in-progress"
       data-sortable-api="/api/tasks/update"
       data-sortable-stage-attr="data-category">
    <h3>🔄 In Progress</h3>
    <div class="kanban-card" draggable="true" data-id="2">
      <h4>พัฒนา API</h4>
      <p>สร้าง REST API สำหรับระบบ</p>
      <span class="badge">Development</span>
    </div>
  </div>

  <div class="kanban-column"
       data-component="sortable"
       data-group="kanban"
       data-category="done"
       data-sortable-api="/api/tasks/update"
       data-sortable-stage-attr="data-category">
    <h3>✅ Done</h3>
    <div class="kanban-card" draggable="true" data-id="3">
      <h4>ติดตั้งเซิร์ฟเวอร์</h4>
      <p>ติดตั้งและตั้งค่าเซิร์ฟเวอร์</p>
      <span class="badge">DevOps</span>
    </div>
  </div>
</div>

<style>
.kanban-board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
  padding: 20px;
}

.kanban-column {
  background: #f8f9fa;
  padding: 15px;
  border-radius: 8px;
  min-height: 500px;
}

.kanban-column h3 {
  margin: 0 0 15px 0;
  padding-bottom: 10px;
  border-bottom: 2px solid #dee2e6;
}

.kanban-card {
  background: white;
  padding: 15px;
  margin: 10px 0;
  border-radius: 6px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  cursor: move;
  transition: box-shadow 0.2s;
}

.kanban-card:hover {
  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}

.kanban-card h4 {
  margin: 0 0 8px 0;
  color: #333;
}

.kanban-card p {
  margin: 0 0 10px 0;
  color: #666;
  font-size: 14px;
}

.badge {
  display: inline-block;
  padding: 4px 8px;
  background: #007bff;
  color: white;
  border-radius: 4px;
  font-size: 12px;
}

.kanban-card.sortable-ghost {
  opacity: 0.5;
  background: #e9ecef;
}
</style>

4. รายการที่มีการยืนยัน

<div data-component="sortable" id="confirmed-list">
  <div draggable="true" data-id="1" class="item">รายการ 1</div>
  <div draggable="true" data-id="2" class="item">รายการ 2</div>
  <div draggable="true" data-id="3" class="item">รายการ 3</div>
</div>

<script>
const list = document.querySelector('#confirmed-list');

list.addEventListener('sortable:end', async (e) => {
  const {item, oldIndex, newIndex} = e.detail;

  // ถ้าตำแหน่งไม่เปลี่ยน ไม่ต้องทำอะไร
  if (oldIndex === newIndex) return;

  // ถามยืนยัน
  const confirmed = await DialogManager.confirm(
    'ต้องการย้ายรายการนี้หรือไม่?'
  );

  if (!confirmed) {
    // ยกเลิก - ย้ายกลับตำแหน่งเดิม
    const items = Array.from(list.children);
    if (oldIndex < newIndex) {
      list.insertBefore(item, items[oldIndex]);
    } else {
      list.insertBefore(item, items[oldIndex + 1]);
    }
    return;
  }

  // บันทึกลำดับใหม่
  await saveOrder();
});

async function saveOrder() {
  const items = Array.from(list.children);
  const order = items.map((item, index) => ({
    id: item.dataset.id,
    position: index
  }));

  await fetch('/api/items/reorder', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({order})
  });

  NotificationManager.success('บันทึกลำดับเรียบร้อย');
}
</script>

แนวทางปฏิบัติที่ดี

✅ ควรทำ

  1. ใช้ data-id เสมอ

    <!-- ดี -->
    <div draggable="true" data-id="123">รายการ</div>
  2. ใช้ปุ่มจับสำหรับการ์ดที่มีปุ่มอื่น

    <!-- ดี - ป้องกันการลากโดยไม่ตั้งใจ -->
    <div data-component="sortable" data-handle=".drag-handle">
     <div draggable="true">
       <span class="drag-handle">⋮⋮</span>
       <button>แก้ไข</button>
       <button>ลบ</button>
     </div>
    </div>
  3. ใช้ CSS classes สำหรับสถานะต่างๆ

    .sortable-ghost {
     opacity: 0.4;
     background: #e3f2fd;
    }
    
    .sortable-drag {
     opacity: 0.8;
     transform: rotate(2deg);
    }
  4. จัดการข้อผิดพลาดจาก API

    container.addEventListener('sortable:api-error', (e) => {
     console.error('API Error:', e.detail.error);
     NotificationManager.error('ไม่สามารถบันทึกได้ กรุณาลองใหม่');
    });
  5. ใช้ group สำหรับ Kanban Board

    <div data-component="sortable" data-group="tasks">...</div>
    <div data-component="sortable" data-group="tasks">...</div>

❌ ไม่ควรทำ

  1. ไม่ใส่ data-id

    <!-- ไม่ดี - API integration จะไม่ทำงาน -->
    <div draggable="true">รายการ</div>
  2. ใช้รายการมากเกินไป

    <!-- ไม่ดี - ช้า, ควรใช้ virtual scrolling -->
    <div data-component="sortable">
     <!-- 1000+ รายการ -->
    </div>
  3. ลืมจัดการสถานะ loading

    // ไม่ดี - ไม่แสดงสถานะกำลังบันทึก
    container.addEventListener('sortable:end', async (e) => {
     await saveToAPI(e.detail);
    });
    
    // ดี - แสดงสถานะ
    container.addEventListener('sortable:end', async (e) => {
     LoadingManager.show();
     try {
       await saveToAPI(e.detail);
       NotificationManager.success('บันทึกสำเร็จ');
     } catch (error) {
       NotificationManager.error('บันทึกล้มเหลว');
     } finally {
       LoadingManager.hide();
     }
    });
  4. ไม่ระบุ group สำหรับ cross-container

    <!-- ไม่ดี - ลากข้ามไม่ได้ -->
    <div data-component="sortable">...</div>
    <div data-component="sortable">...</div>
    
    <!-- ดี -->
    <div data-component="sortable" data-group="shared">...</div>
    <div data-component="sortable" data-group="shared">...</div>

💡 เคล็ดลับ

  1. ใช้ transition สำหรับแอนิเมชั่นที่ลื่นไหล

    .sortable-item {
     transition: transform 0.2s ease;
    }
  2. เพิ่ม visual feedback

    .sortable-chosen {
     box-shadow: 0 4px 12px rgba(0,0,0,0.2);
     transform: scale(1.05);
    }
  3. ใช้ debounce สำหรับ API calls

    let saveTimeout;
    container.addEventListener('sortable:end', (e) => {
     clearTimeout(saveTimeout);
     saveTimeout = setTimeout(() => {
       saveToAPI(e.detail);
     }, 500);
    });
  4. เพิ่ม accessibility

    <div draggable="true"
        role="button"
        tabindex="0"
        aria-label="ลากเพื่อจัดเรียง">
     รายการ
    </div>

สรุป

Sortable เป็นคอมโพเนนต์ที่ทรงพลังและยืดหยุ่น เหมาะสำหรับการสร้าง UI แบบลากและวาง ไม่ว่าจะเป็นรายการง่ายๆ หรือ Kanban Board ที่ซับซ้อน การทำงานร่วมกับ FileElementFactory และ ApiComponent ทำให้สามารถสร้างระบบจัดการข้อมูลที่สมบูรณ์ได้อย่างง่ายดาย

จุดเด่น

  • 🎯 ใช้งานง่ายผ่าน HTML attributes
  • 🔄 รองรับการลากข้ามคอนเทนเนอร์
  • 💾 บันทึกผ่าน API อัตโนมัติ
  • 📱 รองรับทัชสกรีนและคีย์บอร์ด
  • 🎨 ปรับแต่ง CSS ได้ง่าย
  • 🔌 ทำงานร่วมกับคอมโพเนนต์อื่นได้ดี

แหล่งข้อมูลเพิ่มเติม