[實作] 可排序的清單 認識 Drag & Drop API

功能

  • 創建有序列表
  • 隨機產生列表
  • 使用者可以隨意拖曳項目到不同位置
  • 檢查按鈕檢查順序是否正確
  • 錯誤顯示紅色,正確顯示綠色

學習到的知識點

HTML

  • draggable="true"
  • data-* attribute

JS

  • Drag and Drop API
  • .setAttribute()
  • [...]
  • .sort()
  • map()

簡介

Demo

HTML

結構

1
2
3
4
5
6
7
8
9
10
<body>
<h1>TOP 10 World's Most Valuable Brands in 2021</h1>
<p>Drag and drop the items into their corresponding spots</p>
<!-- JS動態產生ul -->
<ul class="draggable-list" id="draggable-list"></ul>
<button class="check-btn" id="check">
Check Order
<i class="fas fa-paper-plane"></i>
</button>
</body>

CSS

完整Code

flex:flex-grow flex-shrink flex-basic

flex: 放大比例 縮小比例 計算元素是否有多餘空間,預設值為auto。

flex:1的意思為flex: 1 1 0,所以數字與<i class="fas fa-grip-lines"></i>的空間,最小最大都會是1,所以不會變形。

1
2
3
4
5
.draggable-list li {
background-color: #fff;
display: flex;
flex: 1;
}
1
2
3
4
5
6
7
8
.draggable {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
flex: 1;
}

JS

完整Code

宣告變數

排序的內容為,2021世界十大最有價值的品牌

原生JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const draggable_list = document.getElementById("draggable-list");
const check = document.getElementById("check");

const powerfulBrands = [
"Apple Inc.",
"Amazon.com Inc.",
"Microsoft",
"Google",
"Samsung",
"Coca-Cola",
"Toyota",
"Mercedes-Benz",
"McDonald’s",
"Disney",
];

// Store listitems
const listItems = [];

let dragStartIndex;
Jquery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const draggable_list = $('#draggable-list')[0]
const powerfulBrands = [
"Apple Inc.",
"Amazon.com Inc.",
"Microsoft",
"Google",
"Samsung",
"Coca-Cola",
"Toyota",
"Mercedes-Benz",
"McDonald’s",
"Disney",
];

const listItems = [];

let dragStartIndex;

將陣列放入DOM裡

展開運算子

在 ES6 中,新增了一個 [ … ] 的關鍵字,這個關鍵字在不同時間點,有不同的效果,有些時候它會被當作展開運算子(spread operator)使用,有時候則是被當作其餘運算子(rest operator)使用。

展開運算子,是把許多的參數轉換成一個陣列,而展開運算子則是可以把陣列中的元素取出

實作電影定位介面時,也有使用到此技巧。

.setAttribute()

Element.setAttribute(name, value)

增加指定名稱的新屬性,或者把一個現有的屬性設定為指定的值。

舉例來說

setAttribute Demo

  1. 用html設定一個Button
1
2
3
<body>
<button>Hello World</button>
</body>
  1. setting設定字為紅色
1
2
3
.setting {
color: red;
}
  1. b變數取得button,再利用setAttribute增加樣式方法屬性
1
2
3
4
5
6
var b = document.querySelector("button");

b.setAttribute("id", "helloButton");
b.setAttribute("class", "setting");
b.setAttribute("onclick", "javascript:alert('test!')");
console.log(b)

HTML5 中的 data-* attribute 屬性

在製作網頁的過程中,我們常常會添加一些自己需要用到的屬性名稱,以方便自己容易理解,為了要避免大家在 HTML 結構中隨意的添加屬性,在 HTML5 中就多了 data-* attribte 這個屬性,其中的 * 就是一個可以自定義的名稱。例如:data-key='83' 或者是 data-item='1'

而這邊的範例就是data-index

原生JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
createList();

// Insert list items into DOM
function createList() {
[...powerfulBrands].forEach((company, index) => {
// listItem為空的陣列,放入powerfulBrands的陣列。
const listItem = document.createElement("li");

listItem.setAttribute("data-index", index);

listItem.innerHTML = `
<span class="number">${index + 1}</span>
<div class="draggable" draggable="true">
<p class="person-name">${company}</p>
<i class="fas fa-grip-lines"></i>
</div>
`;

listItems.push(listItem);

draggable_list.appendChild(listItem);
});
}

顯示出來的樣子

Jquery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.forEach((company, index) => {
const listItem = document.createElement("li");

listItem.setAttribute("data-index", index);

listItem.innerHTML = `
<span class="number">${index + 1}</span>
<div class="draggable" draggable="true">
<p class="company-name">${company}</p>
<i class="fas fa-grip-lines"></i>
</div>
`;

listItems.push(listItem);

draggable_list.appendChild(listItem);
});
addEventListeners();
}

隨機排列item順序

這邊使用的方法是

  1. 先產生隨機數
  2. 再利用sort()方法由小排到大
  3. 完成每次都能產生隨機排列的順序

當只有.map((a) => ({ value: a, sort: Math.random() }))時,person會印出map()賦予的sort,可以注意到都是random( 0 - 1之間 )的值。

.map()每個都取得隨機變數後,利用sort()由小排到大。

排列完大小後,用map()取出值就好。

sort()用法解釋

Array.prototype.sort()

1
2
3
4
5
6
const numbers = [1, 3, 110, 40, 302];
console.log(
numbers.sort(function (a, b) {
return a - b;
})
);

原生JS
1
2
3
4
5
6
7
8
9
[...powerfulBrands]
// array有sort方法,所以利用random產生sort值
.map((a) => ({ value: a, sort: Math.random() }))
// 產生了之後,sort方法比大小,這樣每次重整後的順序都會不同
.sort((a, b) => a.sort - b.sort)
// 只取得value
.map((a) => a.value)
.forEach((person, index) => {
console.log(person)
Jquery
1
2
3
4
5
6
7
8
9
10
11
12
13
[...powerfulBrands]
.map(function (data) {
return {
value: data,
sort: Math.random(),
};
})
.sort(function (a, b) {
return a.sort - b.sort;
})
.map(function (data) {
return data.value;
})

設定事件監聽Function

HTML5 Drag and Drop API 筆記

針對能夠被拖曳的元素,在其 HTML 標籤上添加屬性 draggable="true"

以下整理成表格,方便之後判斷。這次實作沒有運用到dropend

x Drag Source Drag Target 解釋
1 dragstart 開始拖曳元素時觸發此事件()
2 drag dragenter 拖曳元素時觸發此事件
3 dragover 當元素拖曳到有效位置放置則觸發此事件
4 dragleave 拖曳的元素離開有效的位置時觸發
5 drop 有效位置放置元素時觸發此事件
6 dropend 當拖曳結束時會觸發此事件
Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function addEventListeners() {
const draggables = document.querySelectorAll(".draggable");
const dragListItems = document.querySelectorAll(".draggable-list li");

draggables.forEach((draggable) => {
draggable.addEventListener("dragstart", dragStart);
});

dragListItems.forEach((item) => {
item.addEventListener("dragover", dragOver);
item.addEventListener("drop", dragDrop);
item.addEventListener("dragenter", dragEnter);
item.addEventListener("dragleave", dragLeave);
});
}

check.addEventListener("click", checkOrder);

Jquery

Jquery沒有相關的寫法,只有在 jqueryUI裡面才有。所以,以下都只有原生JS的寫法。

dragEnter()dragLeave()

  • dragenter,拖曳元素時觸發的事件。
  • dragleave,拖曳的元素離開有效的位置時觸發。

這邊做出的效果是,當拖曳的元素到其他元素上時,會變色。拖曳第一欄到第二欄上時,第二欄就變色。

但是當拖曳欄位不在有效範圍時,第二欄的欄位就不會變色。

原生JS
1
2
3
4
5
6
7
8
9
function dragEnter() {
// console.log('Event: ', 'dragenter');
this.classList.add("over");
}

function dragLeave() {
// console.log('Event: ', 'dragleave');
this.classList.remove("over");
}

dragStart()dragDrop()

.closet是一個遍歷的方法。

.closetDemo

.getAttribute就是前面提到的,增加指定名稱和值的新屬性,所以可以得到index。

  • dragstart開始拖曳元素時觸發此事件。
  • drop,在有效位置上放置元素時,觸發此事件。

當開始拖曳元素欄位時,就開始記錄Index,停止拖曳元素時,也記錄當下的Index。

再利用swapItems(),交換內容。

原生JS
1
2
3
4
5
6
7
8
9
10
11
12
function dragStart() {
// console.log('Event: ', 'dragstart');
dragStartIndex = +this.closest("li").getAttribute("data-index");
console.log(dragStartIndex)
}

function dragDrop() {
// console.log('Event: ', 'drop');
const dragEndIndex = +this.getAttribute("data-index");
swapItems(dragStartIndex, dragEndIndex);
this.classList.remove("over");
}

當我移動第一個時,dragStartIndex的改變,就會跳出0…以此類推。

dragOver()

避免預設行為,才使用swapItems()避免被觸發提交。

  • dragOver,當元素拖曳到有效位置放置則觸發此事件。

在dragOver函式裡加上避免預設行為的preventDefault(),避免被不停觸發。

原生JS
1
2
3
4
function dragOver(e) {
// console.log('Event: ', 'dragover');
e.preventDefault();
}

SwapItems()

交換items,使用到appendChild方法。

原生JS
1
2
3
4
5
6
7
8
9
//Swap list items that are drag and drop
function swapItems(fromIndex, toIndex) {
const itemOne = listItems[fromIndex].querySelector(".draggable");
const itemTwo = listItems[toIndex].querySelector(".draggable");

// console.log(itemOne, itemTwo);
listItems[fromIndex].appendChild(itemTwo);
listItems[toIndex].appendChild(itemOne);
}

檢查順序是否正確

  • trim()將字串去除空白
原生JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Check the order of list items
function checkOrder() {
listItems.forEach((listItem, index) => {
const randomBrands = listItem.querySelector(".draggable").innerText.trim();

if (randomBrands !== powerfulBrands[index]) {
listItem.classList.add("wrong");
} else {
listItem.classList.remove("wrong");
listItem.classList.add("right");
}
});
}

check.addEventListener("click", checkOrder);

參考文章

JavaScript中setAttribute用法介紹

什麼是 HTML 5 中的資料屬性(data-* attribute)

製作可拖曳的元素(HTML5 Drag and Drop API)

Node Element 在 appendChild 後消失(disappear)!?


[實作] 可排序的清單 認識 Drag & Drop API
https://phoebeho.com/Portfolio/20210201/1586563443/
作者
Phoebe
發布於
2021年2月1日
許可協議