JointJSでAdventCalendarを作成する

ここまで紹介したJointJSの技術をもとにしてAdventCalendarのような図形をJointJSで作成します。

最終的に以下のような図が作成されます。[^1]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129526/fb56b509-a7d6-08d8-5b96-4bacf4489b92.png)

コードはこちら。

https://github.com/acnaman/jointjs-adventcalendar

作成のポイントをいくつかかいつまんで説明します。

## 日ごとのパーツをカスタム要素で作成

AdventCalendarのステータスとして以下3つを想定しました。

1. 未登録
1. 登録予約済み（記事へのリンクは未登録）
1. リンク投稿済み

この3つのステータスに分けて、カスタム要素を作成しています。

```js
/**
 * 参加受付中の日
 */
export const AcceptingDay = dia.Element.define(
  'calendar.AcceptingDay',
  // 中略
);

/**
 * 予約済の日
 */
export const ReservedDay = dia.Element.define(
  'calendar.ReservedDay',
  // 中略
);

/**
 * 投稿済の日
 */
export const PostedDay = dia.Element.define(
  'calendar.PostedDay',
  // 中略
);
```

これらのパーツは、それぞれ以下の条件で出し分けられるとしました。

- 未登録：　タイトル・リンクともに登録無し
- 登録予約済み：　タイトルの登録あり、リンクの登録無し
- リンク投稿済み：　タイトル・リンクともに登録あり

この条件をもとに、3つのパーツをデータから反映して適切なパーツのElementを生成するファクトリメソッドを作成しています。

```js
export const createCalendarDay = function (date, point, title, url) {
  if (!!title && !!url) {
    return PostedDay.create(date, point, title, url);
  }

  if (!!title) {    
    return ReservedDay.create(date, point, title);
  }

  return AcceptingDay.create(date, point);
}
```

## 内部の座標を`calc()`で調整する

これまでの記事で解説が漏れていたのですが、要素の座標やサイズを指定する際に`calc()`という関数を使用できます。これは要素全体の`size`, `position`の設定値を元にした値を参照できる関数です。
例えば`calc(w)`は要素の`width`の値となります。この値を加工することもでき、`calc(w/4)`の場合は`width`の4分の1の値を取得できます。

以下は参加受付中の日を表す`AcceptingDay`のパーツのコードの抜粋です。
`calc()`を使用することで各セレクタの位置をパーツ全体から相対的に指定できるようにしています。

```js
/**
 * 参加受付中の日
 */
export const AcceptingDay = dia.Element.define(
  'calendar.AcceptingDay',
  {
    size: { width: 150, height: 170 },
    attrs: {
      body: {
        width: 'calc(w)',
        height: 'calc(h)',
      },
      'add-icon': {
        width: 'calc(w/4)',
        height: 'calc(w/4)',
        x: 'calc(x + calc(0.375 * w))',
        y: 'calc(y + calc(h/2))',
      },
      date: {
        x: 'calc(x+10)',
        y: 'calc(y+20)',
      },
    }
  },
  {
    markup: util.svg`
      <rect @selector="body" />
      <image @selector="add-icon" />
      <text @selector="date" />
    `
  },
```

## 各日付を編集時に判別させる

今回はどの日付のパーツでも編集ボタン（または＋ボタン）が押されたときは`editCalendar`のイベントを発火させるように設計しています。[^2]
Paperが`editCalendar`イベントを受け取ったとき、その発火元のViewを取得できます。
ここからJavaScript内で持っているデータとの紐づけを行う必要がありますので、Viewから辿ることのできる`prop`に日付のデータをコンストラクタで入れるようにしています。

### コンストラクタ側のコード

```calendar-parts.js
export const AcceptingDay = dia.Element.define(
  // 中略
  {
    create(date, point) {
      const acceptingDay = new AcceptingDay();
      acceptingDay.prop('date', date);  // ここでpropに保持させる
      acceptingDay.attr('date/text', date);
      acceptingDay.position(point.x, point.y);

      return acceptingDay;
    }
  }
});
```

### イベントを受け取ったときのコード

```App.vue
    this.paper.on('editCalendar', function (cellView) {
      const date = cellView.model.attr('date');  // propから日付を取得し
      this.showEditModal(articles.get(date));    // 日付データと紐付ける
    });
```

## 作ってみた所感

**正直、JointJSで作るメリットが薄い！！！** と感じました。
今回はLinkを全く使わなかったため、ただSVGを定義して並べているという感じでした。。。
カスタム要素を使用することで再利用性が高くコード量が抑えられたのは良かったですが、Vueを使ってるならコンポーネントで良くね？って感じになってしまいました。

もしかしたら以下のような要件があった場合はJointJSで作成するメリットがあったかもしれません。

- 「カレンダー日付をドラッグ＆ドロップで動かすことができる」　→ `interactive:true`で自在に動かせる
- 「カレンダーの日付を矢印で繋ぐような見た目にする」→ `Link`を使って繋げられる
- 「クリスマスツリーの形状に合わせて日付を配置する」→ `directedGraph`を使えばいい感じの配置にできそう

## まとめ

今回は今までの知識を応用して実際にAdventCalendarを模した図を作成する方法を紹介しました。
JointJSを学ぶためには実際に何か作ってみるのが手っ取り早いです。習うより慣れろ。まずは身近なサンプルを題材にして作ってみることをおすすめします。

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

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

[^1]: 見た目がちょっと残念ですが、デザインまで調整している時間がありませんでした。。。
[^2]: 編集がクリックされたらダイアログを表示して編集できるようにするつもりでしたがそこまで作れませんでした