[HTML][TypeScript] Sort elements by drag & drop API
Tags: typescript
Published: 2021-03-05 11:47:34

## Intro
This time, I tried sorting DOM elements by drag and drop API

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3oq8lcqwep5a8cz076ft.png)

#### package.json
```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
```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
```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.

```html
<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
```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
```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
```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
```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();
```


## Resources
* [HTML Drag and Drop API - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)
* [draggable - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable)
* [Drag Operations - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations)