Intro
This time, I tried sorting DOM elements by drag and drop API
package.json
{
"dependencies": {
"autoprefixer": "^10.2.4",
"postcss": "^8.2.6",
"postcss-cli": "^8.3.1",
"ts-loader": "^8.0.17",
"tsc": "^1.20150623.0",
"typescript": "^4.2.2",
"webpack": "^5.24.3",
"webpack-cli": "^4.5.0"
}
}
Base project
Home.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello</title>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/home.page.css">
</head>
<body>
<div class="draggable_area" id="draggable_area_1">
<div class="draggable_item" draggable="true" id="draggable_element_1">
<div>
title
</div>
<div>
content
</div>
</div>
<div class="draggable_item" draggable="true" id="draggable_element_2">
<div>
title2
</div>
<div>
content2
</div>
</div>
</div>
<script src="/js/homePage.js"></script>
</body>
</html>
home.page.css
.draggable_area {
border: 1px solid black;
background-color: cadetblue;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 20vh;
width: 40vw;
}
.draggable_item {
border: 1px solid black;
background-color: coral;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
width: 30%;
height: 90%;
}
.dragging {
opacity: 50%;
}
.drop_over {
background-color: cornflowerblue;
}
Implement drag & drop
Drag
I just need to add a "draggable" attribute to make target elements draggable.
<div draggable="true">
</div>
But because I can't drop them anywhere, I can do nearly nothing.
Drop
Because there is no "droppable" attribute what can be used in current web browsers, so I have to implement by my self.
I can use some DOM events related to the "draggable" attribute, so I can get what element was dragged and where it was dropped.
home.page.ts
const parentElement: HTMLElement = document.getElementById('draggable_area_1') as HTMLElement;
function init() {
for(let i = 0; i < parentElement.children.length; i++) {
const targetElement = parentElement.children[i] as HTMLElement;
if(targetElement.classList != null &&
targetElement.classList.contains('draggable_item'))
{
targetElement.ondragstart = ev => handleStartDraggingEvent(ev);
targetElement.ondragover = ev => handleDraggingOverEvent(ev);
targetElement.ondrop = ev => handleDroppingEvent(ev);
}
}
}
function handleStartDraggingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
console.log("[DRAG] " + target.id);
}
function handleDraggingOverEvent(ev: DragEvent) {
// Elements prevent Drop operation by default
ev.preventDefault();
}
function handleDroppingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
console.log("[DROP]" + target.id);
ev.preventDefault();
}
init();
Now I can know what element was dragged in "handleStartDraggingEvent()" and what element was dropped in "handleDroppingEvent()".
But their infomations aren't connected.
So I set data in "handleStartDraggingEvent()" and receive it in "handleDroppingEvent()".
home.page.ts
...
function handleStartDraggingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
console.log("[DRAG] " + target.id);
var dataTransfer = ev.dataTransfer;
if(dataTransfer == null) {
return;
}
dataTransfer.setData("text/plain", target.id);
}
...
function handleDroppingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
console.log("[DROP]" + target.id);
const dragTargetId = ev.dataTransfer!.getData("text/plain");
ev.preventDefault();
}
...
Add decorations and sort elements
Add decorations
- Add transparency when the element is dragging
- Change color when the element is drag-over
- If the element is itself, it avoid changing color
home.page.ts
...
function init() {
for(let i = 0; i < parentElement.children.length; i++) {
const targetElement = parentElement.children[i] as HTMLElement;
if(targetElement.classList != null &&
targetElement.classList.contains('draggable_item'))
{
targetElement.ondragstart = ev => handleStartDraggingEvent(ev);
targetElement.ondragover = ev => handleDraggingOverEvent(ev);
targetElement.ondragenter = ev => handleDraggingEnterEvent(ev);
targetElement.ondragexit = ev => handleDraggingExitEvent(ev);
targetElement.ondrop = ev => handleDroppingEvent(ev);
targetElement.ondragend = ev => handleDropEndEvent(ev);
}
}
}
...
function handleStartDraggingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
console.log("[DRAG] " + target.id);
target.classList.add('dragging');
var dataTransfer = ev.dataTransfer;
if(dataTransfer == null) {
return;
}
dataTransfer.setData("text/plain", target.id);
}
function handleDraggingEnterEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
if(target.id === ev.dataTransfer!.getData("text/plain")) {
return;
}
if(target?.classList == null) {
return;
}
target.classList.add('drop_over');
}
function handleDraggingExitEvent(ev: Event) {
const target = ev.target as HTMLElement;
if(target?.classList == null) {
return;
}
target.classList.remove('drop_over');
}
...
function handleDropEndEvent(ev: Event) {
const target = ev.target as HTMLElement;
if(target?.classList == null) {
return;
}
target.classList.remove('dragging');
if(target.classList.contains('drop_over')){
target.classList.remove('drop_over');
}
}
function handleDroppingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
if(target.classList.contains('drop_over')){
target.classList.remove('drop_over');
}
...
}
One important thing is events like "ondragenter" are not only fired by "div" elements what have "draggable_item" class in this sample, but also their children(text elements).
And they don't have "classList" property.
So when I add or remove class into elements, I must check if the target has "classList" property.
Sort elements
Because there are no any special method to sort elements, so I add the elements in a list, and when I sort them, I clean the parentElement's child elements and add sorted list.
Full ts code
home.page.ts
type DragTarget = {
id: string,
element: HTMLElement,
index: number,
};
const parentElement: HTMLElement = document.getElementById('draggable_area_1') as HTMLElement;
let targets = new Array<DragTarget>();
function init() {
for(let i = 0; i < parentElement.children.length; i++) {
const targetElement = parentElement.children[i] as HTMLElement;
if(targetElement.classList != null &&
targetElement.classList.contains('draggable_item'))
{
targetElement.ondragstart = ev => handleStartDraggingEvent(ev);
targetElement.ondragover = ev => handleDraggingOverEvent(ev);
targetElement.ondragenter = ev => handleDraggingEnterEvent(ev);
targetElement.ondragexit = ev => handleDraggingExitEvent(ev);
targetElement.ondrop = ev => handleDroppingEvent(ev);
targetElement.ondragend = ev => handleDropEndEvent(ev);
targets.push({
id: targetElement.id,
element: targetElement,
index: i,
});
}
}
}
function handleStartDraggingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
console.log("[DRAG] " + target.id);
target.classList.add('dragging');
var dataTransfer = ev.dataTransfer;
if(dataTransfer == null) {
console.error('dataTransfer was null');
return;
}
dataTransfer.setData("text/plain", target.id);
}
function handleDraggingEnterEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
if(target.id === ev.dataTransfer!.getData("text/plain")) {
return;
}
if(target?.classList == null) {
console.log(target);
return;
}
target.classList.add('drop_over');
}
function handleDraggingExitEvent(ev: Event) {
const target = ev.target as HTMLElement;
if(target?.classList == null) {
return;
}
target.classList.remove('drop_over');
}
function handleDraggingOverEvent(ev: DragEvent) {
ev.preventDefault();
}
function handleDropEndEvent(ev: Event) {
const target = ev.target as HTMLElement;
if(target?.classList == null) {
return;
}
target.classList.remove('dragging');
if(target.classList.contains('drop_over')){
target.classList.remove('drop_over');
}
}
function handleDroppingEvent(ev: DragEvent) {
const target = ev.target as HTMLElement;
if(target.classList.contains('drop_over')){
target.classList.remove('drop_over');
}
console.log("[DROP]" + target.id);
const dragTargetId = ev.dataTransfer!.getData("text/plain");
const dropped = targets.find(t => t.id === target.id);
const dragged = targets.find(t => t.id == dragTargetId);
if(dropped == null ||
dragged == null) {
return;
}
const droppedIndex = dropped.index;
dropped.index = dragged.index;
dragged.index = droppedIndex;
updateElements();
ev.preventDefault();
}
function updateElements() {
for(let i = parentElement.children.length - 1; i >= 0; i--) {
parentElement.removeChild(parentElement.children[i]);
}
for(const target of targets.sort((a, b) => a.index - b.index)) {
parentElement.appendChild(target.element);
}
}
init();
Top comments (0)