# 有 jQuery UI
使用 jQuery 建立可動態新增且可調整順序(Sortable)的 Tabs 頁面,最穩定的做法是結合 **jQuery UI** 庫。jQuery UI 內建了 `tabs()` 與 `sortable()` 功能,能用極少的代碼達成複雜的互動。
---
### 1. 快速整合範例
你需要引入 jQuery 與 jQuery UI 的 CSS/JS 檔案。
```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
<style>
#tabs .ui-tabs-nav { cursor: move; } /* 提示使用者可以拖動 */
.tab-content { padding: 20px; border: 1px solid #ccc; margin-top: -1px; }
</style>
</head>
<body>
<div id="tabs">
<ul>
<li><a href="#tab-1">首頁</a></li>
</ul>
<div id="tab-1" class="tab-content">
<p>這是初始頁面。</p>
</div>
</div>
<button id="add-tab" style="margin-top: 20px;">新增頁籤</button>
<script>
$(function() {
// 1. 初始化 Tabs
var tabs = $("#tabs").tabs();
// 2. 使頁籤標題可拖動排序 (Sortable)
tabs.find(".ui-tabs-nav").sortable({
axis: "x",
stop: function() {
tabs.tabs("refresh"); // 排序停止後更新 Tabs 狀態
}
});
// 3. 動態新增功能
var counter = 2;
$("#add-tab").on("click", function() {
var label = "頁籤 " + counter;
var id = "tab-" + counter;
// 新增 li (標題)
var li = `<li><a href="#${id}">${label}</a></li>`;
tabs.find(".ui-tabs-nav").append(li);
// 新增內容區塊
tabs.append(`<div id="${id}" class="tab-content"><p>這是 ${label} 的內容</p></div>`);
// 刷新 Tabs 結構並切換到新頁籤
tabs.tabs("refresh");
tabs.tabs("option", "active", -1); // -1 代表最後一個
counter++;
});
});
</script>
</body>
</html>
```
---
### 2. 核心功能解析
#### **動態新增 (Dynamic Add)**
在 jQuery UI 中,新增 Tab 的邏輯是:
1. 在 `<ul>` 中新增一個 `<li>`。
2. 在容器內新增一個相對應 `id` 的 `<div>`。
3. **關鍵步:** 呼叫 **`tabs("refresh")`**。這會讓 jQuery 重新掃描 DOM,將新加入的 HTML 轉換為功能完整的頁籤。
#### **調整順序 (Sortable)**
這是利用 `tabs.find(".ui-tabs-nav").sortable()`。
* **`axis: "x"`**: 限制只能左右拖動。
* **`refresh`**: 拖動完成後(`stop` 事件),必須通知 Tabs 元件順序已變,否則內部的索引值(Index)可能會錯亂。
---
### 3. 進階優化建議
* **刪除功能**:如果需要刪除 Tab,可以在 `<li>` 內加入一個 `<span>` 圖示,點擊時移除該 `li` 與對應的 `div`,然後再次執行 `refresh`。
* **資料儲存**:若要記憶順序,可以在 `sortable` 的 `update` 事件中,將目前的 ID 順序存入 `localStorage`。
### 💡 為什麼選擇 jQuery UI 而非原生 jQuery 手寫?
雖然手寫不難,但 jQuery UI 幫你處理了以下瑣事:
1. **ARIA 無障礙標籤**:自動處理鍵盤左右鍵切換頁籤。
2. **樣式一致性**:自動處理選中狀態(Active Class)。
3. **Sortable 整合**:處理拖動時的占位符(Placeholder)與 DOM 結構保護。
# 無 jQuery UI
---
### 1. 核心邏輯設計
* **動態建立**:使用 `append()` 插入 HTML,並用 `data-target` 屬性將「頁籤」與「內容區」關聯。
* **切換功能**:透過事件委派 `on('click')` 處理點擊。
* **拖拽排序**:利用 HTML5 的 `draggable="true"` 屬性與 `dragstart`、`dragover` 等事件來實作交換位置。
---
### 2. 完整實作代碼
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
/* 基本頁籤樣式 */
.tabs-nav { display: flex; list-style: none; padding: 0; border-bottom: 2px solid #ccc; margin: 0; }
.tab-item {
padding: 10px 20px; cursor: pointer; border: 1px solid #ccc;
border-bottom: none; background: #eee; margin-right: 5px;
user-select: none; transition: 0.2s;
}
.tab-item.active { background: white; font-weight: bold; border-top: 3px solid blue; }
.tab-item.dragging { opacity: 0.5; border: 2px dashed #000; }
.tabs-content { padding: 20px; border: 1px solid #ccc; border-top: none; }
.content-panel { display: none; }
.content-panel.active { display: block; }
</style>
</head>
<body>
<div id="myTabs">
<ul class="tabs-nav" id="tabNav">
<li class="tab-item active" draggable="true" data-target="panel-1">首頁</li>
</ul>
<div class="tabs-content" id="tabContainer">
<div class="content-panel active" id="panel-1">這是初始頁面內容。</div>
</div>
</div>
<button id="btnAddTab" style="margin-top:20px;">+ 新增頁籤</button>
<script>
$(document).ready(function() {
let tabCount = 1;
// --- 1. 動態新增功能 ---
$('#btnAddTab').on('click', function() {
tabCount++;
const tabId = `panel-${tabCount}`;
const tabTitle = `頁籤 ${tabCount}`;
// 新增標題
$('#tabNav').append(`<li class="tab-item" draggable="true" data-target="${tabId}">${tabTitle}</li>`);
// 新增內容
$('#tabContainer').append(`<div class="content-panel" id="${tabId}">這是 ${tabTitle} 的內容</div>`);
// 自動切換到新頁籤
$(`[data-target="${tabId}"]`).click();
});
// --- 2. 切換頁籤功能 (事件委派) ---
$('#tabNav').on('click', '.tab-item', function() {
const target = $(this).data('target');
$('.tab-item').removeClass('active');
$(this).addClass('active');
$('.content-panel').removeClass('active');
$(`#${target}`).addClass('active');
});
// --- 3. 拖拽排序功能 (HTML5 Drag & Drop) ---
let dragElement = null;
$('#tabNav').on('dragstart', '.tab-item', function(e) {
dragElement = this;
$(this).addClass('dragging');
});
$('#tabNav').on('dragover', '.tab-item', function(e) {
e.preventDefault(); // 必須阻止預設行為才能觸發 drop
const overElement = this;
if (overElement !== dragElement) {
// 判斷拖拽位置,動態交換 DOM 順序
const children = Array.from(this.parentNode.children);
const dragIdx = children.indexOf(dragElement);
const overIdx = children.indexOf(overElement);
if (dragIdx < overIdx) {
$(overElement).after(dragElement);
} else {
$(overElement).before(dragElement);
}
}
});
$('#tabNav').on('dragend', '.tab-item', function() {
$(this).removeClass('dragging');
dragElement = null;
});
});
</script>
</body>
</html>
```
---
### 3. 重點技術說明
#### **HTML5 拖放事件 (Drag and Drop)**
我們使用了三個核心事件:
1. **`dragstart`**: 當使用者開始拖動頁籤時,記錄下被拖動的物件並改變透明度。
2. **`dragover`**: 這是最關鍵的一步。當被拖動的物件經過其他頁籤時,我們直接在 DOM 中使用 `after()` 或 `before()` 移動節點。這樣你不需要重新計算陣列索引,瀏覽器會自動即時更新視覺順序。
3. **`e.preventDefault()`**: 預設情況下,元素是不允許被放置的,必須在 `dragover` 中呼叫此方法才能讓功能運作。
#### **事件委派 (Event Delegation)**
因為頁籤是動態新增的,如果直接寫 `$('.tab-item').click(...)`,新頁籤會沒有反應。所以必須寫成:
`$('#tabNav').on('click', '.tab-item', function() { ... })`
這樣無論後來新增多少個頁籤,jQuery 都會透過父元素 `#tabNav` 捕捉到點擊事件。
### 💡 進階提示:如何儲存順序?
如果你希望使用者重新整理頁面後順序還在,可以在 `dragend` 事件中執行以下邏輯:
```javascript
const order = [];
$('.tab-item').each(function() {
order.push($(this).data('target'));
});
localStorage.setItem('tabOrder', JSON.stringify(order));
```