svelte-dnd-actionを使ってsvelteでドラッグアンドドロップを実装

# はじめに

カンバンボードのようなものを作りたく、
ドラッグアンドドロップのツールを探していました。

公式でドラッグアンドドロップのonイベントは用意されていますが、もう少し簡単に実装できないかと思い、
`svelte-dnd-action`を使ってみました。

[公式の例](https://svelte.dev/repl/adf5a97b91164c239cc1e6d0c76c2abe?version=3.14.1)

## svelte-dnd-action導入

```bash
$ npm install --save-dev svelte-dnd-action
```

## 使い方

まずは参考コードから。
```vue
<script lang="ts">
  const myItems = {
    id: 1, name: 'item1',
  }
</script>

<div
  use:dndzone="{{items: myItems, ...otherOptions}}"
  on:consider="{handler}"
  on:finalize="{handler}"
>
  {#each myItems as item(item.id)}
      <div>{item.name}</div>
  {/each}
</div>
```

`<div>`タグに注目すると、`use:dndzone`、`on:consider`、`on:finalize`を発見。

```vue
<div
  use:dndzone="{{items: myItems, ...otherOptions}}"
  on:consider="{handler}"
  on:finalize="{handler}"
>
```

この3つをドラッグアンドドロップさせたい範囲に設定することでドラッグアンドドロップを機能させています。

### `use:dndzone`

ドラッグアンドドロップをさせたい配列を指定するメソッド。
`otherOptions`には文字通り、オプションを指定する。
オプションは以下の通り

### オプション

Name | Type | 必須 | デフォルト
-----|------|----|------
flipDurationMs | Number | No | 0
type | String | No | Internal
dragDisabled | Boolean | No | false
morphDisabled | Boolean | No | false
dropFromOthersDisabled | Boolean | No | false
zoneTabIndex | Number | No | 0
dropTargetStyle | Object<String> | No | {outline: 'rgba(255, 255, 102, 0.7) solid 2px'}
dropTargetClasses | Array<String> | No | []
transformDraggedElement | Function | No | () => {}
autoAriaDisabled | Boolean | No | false
centreDraggedOnCursor | Boolean | No | false

よく使いそうなものを解説していきます。

#### `flipDurationMs` 

ドラッグされたアイテムが戻るときのアニメーションの秒数。
0または設定しないとアニメーションがないことにできる

0の場合
![0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/0f4f7e52-2cc4-0bf9-5169-f7fe1fe58ed4.gif)

10000の場合(10秒かけて戻る)
![10000.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/53904cdf-d155-499b-0299-3184cdc7ddf7.gif)

#### `type`

同じタイプが設定されているdnd-zone同士ではドロップアンドドロップできます。
デフォルトではすべてのdnd-zoneが同じタイプになってます。

#### `dragDisabled`

設定するとドラッグアンドドロップを禁止できる

#### `dropFromOthersDisabled`

他のdnd-zoneからのドロップを許可するかどうか。
こちらからのドラッグは制限しない。

#### `centreDraggedOnCursor`

trueにするとドラッグした際に、アイテムの中心を持つようにアイテムが移動します。

![center.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/e764d29e-9e70-8509-c955-7a94535bb79d.gif)


## カンバンツールっぽいものを実装してみた

以上を使って実装したのがこちら。

UIコンポーネントには[Flowbite-svelte](https://flowbite-svelte.com/)を利用しました。
導入方法は前回の記事で公開しています。

https://qiita.com/doyaseizin/items/6e7e8571cc6c3d7ad2e8

![kanban.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/1cee84cc-4bf9-0d5d-96ec-f5a19aaecfa2.gif)


```typescript
// handler.ts
export const DndConsider = (e, items) => {
  items = e.detail.items;

  return items
}
export const DndFinalize = (e, items) => {
  items = e.detail.items;

  return items
}
```

```vue
// Dnd.svelte
<script>
  import {flip} from "svelte/animate";
  import {dndzone} from "svelte-dnd-action";
  import { Card } from "flowbite-svelte";
  import { DndConsider, DndFinalize } from "./handler";

  let Aitems = [
      {id: 1, name: "Aitem1"},
      {id: 2, name: "Aitem2"},
      {id: 3, name: "Aitem3"},
      {id: 4, name: "Aitem4"}
  ];

  let Bitems = [
      {id: 5, name: "Bitem1"},
      {id: 6, name: "Bitem2"},
      {id: 7, name: "Bitem3"},
      {id: 8, name: "Bitem4"}
  ];

  const flipDurationMs = 300;
</script>

<div class="flex">
  <div class="w-1/2">
    <h1 class="text-4xl font-bold text-center">配列A</h1>
    <div
      class="box mx-auto"
      use:dndzone="{{items: Aitems, centreDraggedOnCursor: true, flipDurationMs}}"
      on:consider="{e => Aitems = DndConsider(e, Aitems)}"
      on:finalize="{e => Aitems = DndFinalize(e, Aitems)}"
    >
      {#each Aitems as item(item.id)}
        <div class="my-2" animate:flip="{{duration: flipDurationMs}}">
          <Card class="mx-auto">
          {item.name}
          </Card>
        </div>
      {/each}
    </div>
  </div>
  <div class="w-1/2">
    <h1 class="text-4xl font-bold text-center">配列B</h1>
    <div
      class="box mx-auto"
      use:dndzone="{{items: Bitems, centreDraggedOnCursor: true, flipDurationMs}}"
      on:consider="{e => Bitems = DndConsider(e, Bitems)}"
      on:finalize="{e => Bitems = DndFinalize(e, Bitems)}"
    >
      {#each Bitems as item(item.id)}
        <div class="my-2" animate:flip="{{duration: flipDurationMs}}">
          <Card class="mx-auto">
          {item.name}
          </Card>
        </div>
      {/each}
    </div>
  </div>
</div>

<style>
  .box {
    width: 400px;
    border: 1px solid black;
  }
</style>
```