【JointJS】ドラッグ中のCellのイメージを表示する

通常のHTMLでは[`ドラッグ＆ドロップAPI`](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API)を用いることでドラッグ中のイメージを表示することができます。

以下は画像をドラッグした場合の挙動です。

![dadlink.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129526/e397d7e5-eddb-8bed-1e5f-2b11b3ebc756.gif)

ですがこのドラッグ＆ドロップAPIは現時点でSVGでは動作しません。
そのため、JointJSで描画されるSVGの図では上記のような挙動が発生しません。

## でもやりたい

JointJSの場合、まずPaperに`interactive=true`が設定されている場合、ドラッグしたElementはカーソルに合わせて移動します。この場合は問題なし。

今回はPaperに`interactive=false`が設定されている状況を考えます。Elementを元の位置に残した上で他の位置に移動するような動作です。

前述の通りドラッグ＆ドロップAPIは使用できないため、似たようなことを実現するために小細工をします。
最終的には以下のような動作になります。

![dadlink.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129526/df317d9b-75f3-e54e-fe4f-73f0882eceda.gif)

## 実装内容の説明

今回の実装のポイントとなる点をいくつかご紹介します。

### ポイント①：GraphとPaperを2つ用意する

ドラッグ中の動作を実現するために、元の図を表示するGraph/Paperとは別に、フローティングイメージを表示するためのGraph/Paperを用意しています。

```js
// 本体
const graph = new joint.dia.Graph({}, { cellNamespace: namespace });
const paper = new joint.dia.Paper({
  el: document.getElementById('myholder'),
  model: graph,
  width: 600,
  height: 300,
  gridSize: 1,
  interactive: false,
  cellViewNamespace: namespace
});

// フローティングイメージ
const graph_floating = new joint.dia.Graph({}, { cellNamespace: namespace });
const paper_floating = new joint.dia.Paper({
  el: document.getElementById('floating-image'),
  model: graph_floating,
  width: 600,
  height: 300,
  gridSize: 1,
  interactive: false,
  cellViewNamespace: namespace
});
```

それに伴いPaperに対応するHTML要素も2つ用意しています。

```html
<div id="myholder"></div>
<div id="floating-image"></div>
```

### ポイント②：フローティングイメージ用の要素のCSS調整

フローティングイメージはドラッグ中以外は表示する必要がないため`display:none`を指定しておきます。ドラッグ中のみ`display:block`が有効になるようにします。あとは薄く表示されるように`opacity`を指定していたり、余計な背景が表示されないように`background:none`を指定していたりします。

また、本体とフローティングイメージがピッタリと重なるように、`position:absolute`を指定しています。

```css
#myholder {
  position: absolute;
}

#floating-image {
  opacity: 0.3;
  display: none;
  position: absolute;
  background: none;
}

#floating-image.dragging {
  display: block
}
```

### ポイント③：カーソル移動に合わせてフローティングイメージを動かす

カーソルが移動した際にそれに追随してフローティングイメージが動くように、mousemoveイベントで常にフローティングイメージの座標をマウスの位置に合わせるようにしています。

```js
document.addEventListener('mousemove', (evt) => {
  const element = document.getElementById('floating-image');
  element.style.left = evt.pageX + 'px';
  element.style.top = evt.pageY + 'px';
});
```

### ポイント④：ドラッグ開始時のイベントを定義する

本体側のCellがドラッグ開始されたときに、以下の3つの処理を行っています。

1. ドラッグ中フラグのclassを付与
2. ドラッグするElementをフローティングイメージにクローン
3. フローティングイメージの位置調整

イベント内のコード全体は以下となります。

```js
paper.on('cell:pointerdown', function(cellView, evt, x, y) {
  const floatingPaper = document.getElementById("floating-image");
  
  // ドラッグ中フラグのclassを付与
  floatingPaper.classList.add("dragging");

  // フローティングイメージ表示用のGraphにドラッグするElementをクローンする
  graph_floating.clear();
  const clone = cellView.model.clone();
  graph_floating.addCell(clone);
  
  // ちょうど掴んだところにフローティングイメージが表示されるようにPaperの位置を調整する
  floatingPaper.style.transform=`translate(-${x}px, -${y}px)`;
});
```

細かく内容を説明します。

#### 1. ドラッグ中フラグのclassを付与

前述のCSSで、ドラッグ中のみ`display:block`を有効にするためのclassのCSSを作成していました。そのclassをここで設定します。

```js
floatingPaper.classList.add("dragging");
```

#### 2. ドラッグするElementをフローティングイメージにクローン

ドラッグしているCellをフローティングイメージのPaperに表示できるようにするために、Cellをクローンして生成したオブジェクトをフローティングイメージ用のGraphに追加します。

```js
graph_floating.clear();
const clone = cellView.model.clone();
graph_floating.addCell(clone);
```

#### 3. フローティングイメージの位置調整

Cellをクローンするとpositionの設定値も引き継がれますが、[前述](#カーソル移動に合わせてフローティングイメージを動かす)の設定でPaperの位置が動いているため、何もしないとカーソルからCellのposition分ズレた位置に描画されてしまいます。
Paperの座標を`transform`でズラすことで調整を行います。

``` js
floatingPaper.style.transform=`translate(-${x}px, -${y}px)`;
```

### ポイント⑤：ドラッグ終了時のイベントを定義する

ドラッグ中を表すclassの`dagging`は不要になるため、削除します。

```js
paper.on('cell:pointerup', function(cellView) {
  const floatingPaper = document.getElementById("floating-image");
  floatingPaper.classList.remove("dragging");
});
```

## まとめ

今回はドラッグ中のCellイメージを表示する実装例を紹介しました。
`interactive:false`を指定した上でCellをドラッグさせるケースはあまり多くないかもしれませんが、Graph/Paperを2つ用意するなどの方法は他にも応用が効くテクニックとなりますので、ご参考になれば幸いです。

なお、全体のソースコードは以下から確認できます。

https://codepen.io/acnaman/pen/ExMxNqB

※この記事は [JointJS Advent Calendar 2023](https://qiita.com/advent-calendar/2023/jointjs) の記事です。他の記事を読む場合はカレンダーのページを参照してください。

https://qiita.com/advent-calendar/2023/jointjs