Now.js Framework Documentation
Sortable - คอมโพเนนต์ลากและวาง
Sortable - คอมโพเนนต์ลากและวาง
เอกสารฉบับนี้อธิบาย Sortable ซึ่งเป็นคอมโพเนนต์สำหรับการลากและวางเพื่อจัดเรียงรายการ รองรับการลากระหว่างคอนเทนเนอร์ และสามารถบันทึกข้อมูลผ่าน API อัตโนมัติได้
📋 สารบัญ
- ภาพรวม
- การติดตั้งและนำเข้า
- การใช้งานพื้นฐาน
- คุณสมบัติ HTML
- ตัวเลือกการกำหนดค่า
- การลากด้วยปุ่มจับ vs พื้นที่ทั้งหมด
- การลากข้ามคอนเทนเนอร์
- การบันทึกผ่าน API
- การทำงานร่วมกับ FileElementFactory
- การทำงานร่วมกับ ApiComponent
- อีเวนต์
- JavaScript API
- ตัวอย่างการใช้งาน
- แนวทางปฏิบัติที่ดี
ภาพรวม
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 componentdraggable="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"- ใช้ fieldidเป็น 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>แนวทางปฏิบัติที่ดี
✅ ควรทำ
-
ใช้ data-id เสมอ
<!-- ดี --> <div draggable="true" data-id="123">รายการ</div> -
ใช้ปุ่มจับสำหรับการ์ดที่มีปุ่มอื่น
<!-- ดี - ป้องกันการลากโดยไม่ตั้งใจ --> <div data-component="sortable" data-handle=".drag-handle"> <div draggable="true"> <span class="drag-handle">⋮⋮</span> <button>แก้ไข</button> <button>ลบ</button> </div> </div> -
ใช้ CSS classes สำหรับสถานะต่างๆ
.sortable-ghost { opacity: 0.4; background: #e3f2fd; } .sortable-drag { opacity: 0.8; transform: rotate(2deg); } -
จัดการข้อผิดพลาดจาก API
container.addEventListener('sortable:api-error', (e) => { console.error('API Error:', e.detail.error); NotificationManager.error('ไม่สามารถบันทึกได้ กรุณาลองใหม่'); }); -
ใช้ group สำหรับ Kanban Board
<div data-component="sortable" data-group="tasks">...</div> <div data-component="sortable" data-group="tasks">...</div>
❌ ไม่ควรทำ
-
ไม่ใส่ data-id
<!-- ไม่ดี - API integration จะไม่ทำงาน --> <div draggable="true">รายการ</div> -
ใช้รายการมากเกินไป
<!-- ไม่ดี - ช้า, ควรใช้ virtual scrolling --> <div data-component="sortable"> <!-- 1000+ รายการ --> </div> -
ลืมจัดการสถานะ 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(); } }); -
ไม่ระบุ 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>
💡 เคล็ดลับ
-
ใช้ transition สำหรับแอนิเมชั่นที่ลื่นไหล
.sortable-item { transition: transform 0.2s ease; } -
เพิ่ม visual feedback
.sortable-chosen { box-shadow: 0 4px 12px rgba(0,0,0,0.2); transform: scale(1.05); } -
ใช้ debounce สำหรับ API calls
let saveTimeout; container.addEventListener('sortable:end', (e) => { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { saveToAPI(e.detail); }, 500); }); -
เพิ่ม accessibility
<div draggable="true" role="button" tabindex="0" aria-label="ลากเพื่อจัดเรียง"> รายการ </div>
สรุป
Sortable เป็นคอมโพเนนต์ที่ทรงพลังและยืดหยุ่น เหมาะสำหรับการสร้าง UI แบบลากและวาง ไม่ว่าจะเป็นรายการง่ายๆ หรือ Kanban Board ที่ซับซ้อน การทำงานร่วมกับ FileElementFactory และ ApiComponent ทำให้สามารถสร้างระบบจัดการข้อมูลที่สมบูรณ์ได้อย่างง่ายดาย
จุดเด่น
- 🎯 ใช้งานง่ายผ่าน HTML attributes
- 🔄 รองรับการลากข้ามคอนเทนเนอร์
- 💾 บันทึกผ่าน API อัตโนมัติ
- 📱 รองรับทัชสกรีนและคีย์บอร์ด
- 🎨 ปรับแต่ง CSS ได้ง่าย
- 🔌 ทำงานร่วมกับคอมโพเนนต์อื่นได้ดี
แหล่งข้อมูลเพิ่มเติม
- ApiComponent - การทำงานร่วมกับ API
- FileElementFactory - การจัดการไฟล์
- ComponentManager - ระบบคอมโพเนนต์