先日公開した”スクラッチからVue.jsで作る自作ガントチャート”の続きです。

前回までの記事で下記のガントチャートを作成しています。本記事ではイベントを利用してタスクバーの移動やタスクバーの幅、タスクの並び替えなどインタラクティブな機能を実装していきます。

ホイールスクロールで隠れら領域を表示
前回の記事で作成したガントチャート
”スクラッチからVue.jsで作る自作ガントチャート”は、ライブラリの力を借りず自分の力でガントチャート を作ることを目的にした特集です。
fukidashi

マウスイベントによるDrag&Drop

マウスイベントを利用してカレンダー領域に表示されているタスクバーの移動を実装します。タスクバーがマウスイベントを利用して移動できるだけでなく移動と同時に開始日と完了期限日の更新を行います。タスクバーは横移動(X軸)のみできるものとして設定を行います。

マウスイベントにはmousedown, mousemoveとmouseupを利用します。

マウスイベントを利用することでDrag&Drop機能に縦方法、横方向の移動制限を行うことができます。
fukidashi

mousedownイベント

タスクバーの要素にmousedownイベントを設定します。タスクバーをクリックするとメッセージが表示されるかmousedownイベントの動作確認を行います。mousedownイベントにはmouseDownMoveメソッドを指定しています。


<div id="gantt-bar-area" class="relative" :style="`width:${calendarViewWidth}px;height:${calendarViewHeight}px`">
    <div v-for="(bar,index) in taskBars" :key="index">
      <div :style="bar.style" class="rounded-lg absolute h-5 bg-yellow-100" v-if="bar.task.cat === 'task'" @mousedown="mouseDownMove" >

mousedownイベントに指定したmouseDownMoveメソッドをVue.jsのmethods内に追加します。


mouseDownMove(){
  console.log('mouseDownMove')
}

カレンダー領域にあるタスクバーをクリックするとブラウザのデベロッパーツールのコンソールにmouseDownMoveが表示されればmousedownイベントの設定は正常に行われています。

クリックした要素の情報保存

クリックしたタスクバーを移動させるためにクリックしたタスクバーに関する情報を保存しておく必要があります。クリックした位置情報、クリックした要素、タスクのidを保存するデータプロパティを追加します。タスクバーがドラッグされているのかどうかの状態を表すdraggingをデータプロパティに追加します。


data(){
  return {
//略
    dragging:false,
    pageX:'',
    elememt:'',
    left:'',
    task_id:'',

<div :style="bar.style" class="rounded-lg absolute h-5 bg-yellow-100" v-if="bar.task.cat === 'task'"
              @mousedown="mouseDownMove(bar.task)" >

保存する各種情報はeventオブジェクトを利用し、X座標、要素、クリック時の要素のleftの位置情報を取得しています。


mouseDownMove(task){
  this.dragging = true;
  this.pageX = event.pageX;
  this.element = event.target;
  this.left = event.target.style.left;
  this.task_id = task.id
  console.log('mouseDownMove')
}

動作確認のためクリック時のevent.targetとevent.pageXで取得できる値を確認します。


mouseDownMove(task){
  this.dragging = true;
  this.pageX = event.pageX;
  this.element = event.target;
  this.left = event.target.style.left;
  this.task_id = task.id
  console.log(event.pageX)
  console.log(event.target)
}

クリックするとタスクバーのX座標とクリックした要素が表示されます。

クリックした要素とX座標の取得
クリックした要素とX座標の取得

event.targetについてはクリックを行った要素の子要素の情報が取得されています。子要素でmousedownイベントが発生しないように子要素にpoingter-events:noneを設定します。


<div class="w-full h-full" style="pointer-events: none;">

設定後、再度クリックを行うとクリックした要素の情報を取得することができます。異なる要素を3回クリックしています。

クリックした要素の取得
クリックした要素の取得
コンソールのログからクリックした要素によってtop, left, widthの値が異なることが確認できます。
fukidashi

クリックした要素に関する情報を取得することができましたがここまでの設定では、タスクバーを移動させることはできません。

mousemove, mouseupイベントを利用することでタスクバーの移動を実装していきます。

mousemoveイベント

ブラウザ上でマウスを動かしている場合、mousemoveイベントで現在のマウスの座標を取得することができます。

クリックした要素の座標の位置とマウスの現在地の座標の差分を取り、クリックした要素に対してその差分の距離を適用することでマウスの移動に合わせてタスクバーを移動させることができます。

画面上どこでもmousemoveイベントを取得できるようにイベントリスナーへの登録を行います。これまでに設定してきたイベントリスナーと同様にライフサイクルフックのmounted内で設定します。


mounted() {
  //略
  window.addEventListener('resize', this.getWindowSize);
  window.addEventListener('wheel', this.windowSizeCheck);
  window.addEventListener('mousemove', this.mouseMove);
}

mouseMoveメソッドを追加し、以下の設定を行います。


mouseMove() {
  if (this.dragging) {
    let diff = this.pageX - event.pageX;
    this.element.style.left = `${parseInt(this.left.replace('px', '')) - diff}px`;
  }
},

this.leftにはタスクバーをクリックした時のタスクバー要素のleftの値が保存されており、クリック時のleftの値にX座標の差分(diff)を加えた値を要素のleftの値に入れることで移動を行っています。

diffの値はマイナス、プラスのどちらの値もとります。また本アプリケーションではleftのみ設定を行っていますが、topも設定することで縦方向に移動させることも可能です。
fukidashi

設定後タスクバーをクリックしてマウスを移動させるとマウスと一緒にタスクバーが左右に移動します。他のタスクバーをクリックすると今度はクリックしたタスクバーがマウスの移動に合わせて左右に動きます。

これでタスクバーを移動させることができました。次は移動したタスクバーを移動先で止めるためにmouseupイベントを利用します。

mouseupイベント

mouseupイベントはクリックボタンから指を外した時に発火されるイベントです。画面上どこでもmouseupイベントを取得できるようにイベントリスナーへの登録を行います。他のイベントリスナーと同様にライフサイクルフックのmounted内で設定します。


mounted() {
  //略
  window.addEventListener('mousemove', this.mouseMove);
  window.addEventListener('mouseup', function(){
    console.log('mouseup');
  });
}

設定後画面上でクリックして手を外すとデベロッパーツールのコンソールにmouseupのメッセージが表示されます。

マウスの動きとの同期を解除

mousedownイベントでクリックしたタスクバーはマウスを動かすとマウスと一緒に横移動できましたが、他のタスクバーをクリックする以外にマウスとの同期を解除する方法がありませんでした。マウスのクリックを外すとマウスの移動との同期を解除できるようにstopDragメソッドを追加し、その中でドラッグ中かどうかの状態を表すthis.draggingをfalseに設定します。


mounted() {
  //略
  window.addEventListener('mousemove', this.mouseMove);
  window.addEventListener('mouseup', this.stopDrag);
}
mouseMoveメソッドはdraggingがtrueの場合のみに移動の処理を実行するのでdraggingがfalseになると移動処理を行うことができません。
fukidashi

クリックを外すとstopDragメソッドによりthis.draggingの値がfalseに設定されます。


stopDrag(){
  this.dragging = false;
}

設定後、タスクバーをクリックして移動し、クリックを外すとタスクバーはクリックを外した位置に停止させることができます。ここまででタスクバーの移動の実装ができました。

Drag&Dropによる日付の更新

クリックしたタスクバーをマウスの移動に合わせて左右に移動することができるようになりました。しかし、移動が完了してもタスクに設定されている開始日と完了期限日は更新されません。タスクバーの移動に合わせて日付を更新できるようにstopDragメソッドを更新します。


stopDrag(){
  if (this.dragging) {
    let diff = this.pageX - event.pageX
    let days = Math.ceil(diff / this.block_size)
    if (days !== 0) {
      console.log(days)
      let task = this.tasks.find(task => task.id === this.task_id);
      let start_date = moment(task.start_date).add(-days, 'days')
      let end_date = moment(task.end_date).add(-days, 'days')
      task['start_date'] = start_date.format('YYYY-MM-DD')
      task['end_date'] = end_date.format('YYYY-MM-DD')
    } else {
      this.element.style.left = `${this.left.replace('px', '')}px`;
    }
  }
  this.dragging = false;
}

ドラッグ中の状態を表すdraggingがtrueの場合のみ処理が行われます。

mousemoveと同様にX座標の差分(diff)をとりますが、その直後にblock_sizeで差分の移動距離を割っています。mousemoveではタスクバーを下記のように日付の途中でも停止することができました。途中で停止させないようにblock_sizeを利用して移動を日付に変換しています。

mousemoveはどこでも停止できる
mousemoveはどこでも停止できる
上から2つのタスクバーはDrag&Dropの結果、日付の途中からバーが開始しています。本ガントチャートは日付単位で設定するので3番目のように日付の縦線から開始されなければいけません。
fukidashi

block_sizeで割ることで移動が何日分進んだかを計算しています。Math.ceilで少数以下を切り上げているので移動幅が小さい場合は移動の日数を保存するdaysは0になります。その時は元の位置に戻るように設定を行っています。

task_idを利用してtasksのタスク一覧から日付を更新したいタスクを取り出し、start_date、end_dateを更新しています。

stopDragの更新が完了したら動作確認を行います。移動前は各タスクの開始日が縦線にきっちりそろっています。

移動前のタスクバーの状態
移動前のタスクバーの状態

テスト1のタスクバーをドラッグ&ドロップするとタスクバーの移動だけではなく開始日と完了期限日も更新されます。開始日も日付の縦線に沿うように調整が行われ縦線に合わせて表示されます。

タスクバー移動後の状態
タスクバー移動後の状態

タスクバーの移動だけではなく日付の更新と位置の調整が行えるDrag&Drop機能を実装することができました。

タスクバーの拡大・縮小

Drag&Dropによるタスクバーの移動により、開始日と完了期限日の更新を行うことができるようになりました。次はタスクバーを移動させるのではなくタスクバーの幅を拡大、縮小する機能を実装していきます。タスクバーの幅を広げればタスクの日数が増え、タスクバーの幅が狭まればタスクの日数が減少します。

拡大・縮小ボタンの追加

タスクバーを拡大・縮小するために利用する四角いボタンをタスクバーの両端に設定します。追加した四角いボタンの要素は、CSSのpositionプロパティをabsoluteに設定し、topとleftで位置を調整しています。


<div id="gantt-bar-area" class="relative" :style="`width:${calendarViewWidth}px;height:${calendarViewHeight}px`">
  <div v-for="(bar,index) in taskBars" :key="index">
    <div :style="bar.style" class="rounded-lg absolute h-5 bg-yellow-100" v-if="bar.task.cat === 'task'" @mousedown="mouseDownMove(bar.task)">
      <div class="w-full h-full" style="pointer-events: none;">
      </div>
//以下を追加 四角いボタン
      <div class="absolute w-2 h-2 bg-gray-300 border border-black" style="top:6px;left:-6px;cursor:col-resize">
      </div>
      <div class="absolute w-2 h-2 bg-gray-300 border border-black" style="top:6px;right:-6px;cursor:col-resize">
      </div>
    </div>
  </div>
</div>

ブラウザで確認すると下記のようにタスクバーの両端に四角いボタンが表示されます。

タスクバーに拡大・縮小ボタンを付与
タスクバーに拡大・縮小ボタンを付与

mousedownイベントの設定

両端の四角いボタンをクリックするとイベントが発生するようにmousedownイベントを設定します。

左右どちらにもmousedownイベントを設定しイベントにはmouseDownResizeメソッドを指定しますが、右と左では処理が変わるので引数にどちらの四角いボタンがクリックされたかわかるようにleftかrightを入れます。


<div class="absolute w-2 h-2 bg-gray-300 border border-black" 
      style="top:6px;left:-6px;cursor:col-resize" 
      @mousedown="mouseDownResize(bar.task,'left')">
</div>
<div class="absolute w-2 h-2 bg-gray-300 border border-black" 
      style="top:6px;right:-6px;cursor:col-resize" 
      @mousedown="mouseDownResize(bar.task,'right')">
</div>

Vue.jsのmethodsにmouseDownResizeメソッドを追加します。mouseDownMoveではdraggingプロパティを利用しましたが、拡大・縮小ではleftResizingとrightResizingのデータプロパティを新たに追加します。クリック時に渡されるdirectionによってどちらかの値をfalseからtrueに変更します。

mouseDownMoveメソッドと同様にクリックした要素の情報を保存しますが、取得したい要素はこの四角いボタンの要素ではなくタスクバー要素のためparentElementを利用しています。また今回はタスクバーの横幅のサイズを変更するためクリック時のwidthも保存します。


data(){
  return {
//略
    dragging:false,
    pageX:'',
    elememt:'',
    task_id:'',
    width:'', //追加
    leftResizing:false,//追加
    rightResizing:false,//追加

mouseDownResize(task, direction) {
  direction === 'left' ? this.leftResizing = true : this.rightResizing = true;
  this.pageX = event.pageX;
  this.width = event.target.parentElement.style.width;
  this.left = event.target.parentElement.style.left;
  this.element = event.target.parentElement;
  this.task_id = task.id
},

四角いボタンをクリックすると親要素に設定されているmouseDownMoveイベントも発火されるため、stopPropagationを設定します。Vue.jsでは@mousemove.stop=”mouseDownResize(task, direction)”と設定することでstopPropagationを設定することができます。


<div class="absolute w-2 h-2 bg-gray-300 border border-black" 
      style="top:6px;left:-6px;cursor:col-resize" 
      @mousedown.stop="mouseDownResize(bar.task,'left')">
</div>
<div class="absolute w-2 h-2 bg-gray-300 border border-black" 
      style="top:6px;right:-6px;cursor:col-resize" 
      @mousedown.stop="mouseDownResize(bar.task,'right')">
</div>
mouseDownResizeメソッドの中でevent.stopPropagationと設定することも可能です。
fukidashi

クリック後にマウスの移動でタスクバーの大きさを拡大・縮小するためにmousemoveイベントをイベントリスナーに追加します。イベントが発火するとmouseResizeメソッドを実行します。


window.addEventListener('mousemove', this.mouseResize);

まず左端の四角をクリックした場合のみ設定を行います。クリック時の位置と現在のマウスの差分を計算してタスクバーの要素のwidthとleftを更新します。タスクバーの幅が一日分よりも小さくならないようにblock_sizeでwidthのチェックを行っています。


mouseResize() {
  if (this.leftResizing) {
    let diff = this.pageX - event.pageX
    if (parseInt(this.width.replace('px', '')) + diff > this.block_size) {
      this.element.style.width = `${parseInt(this.width.replace('px', '')) + diff}px`
      this.element.style.left = `${this.left.replace('px', '') - diff}px`;
    }
  }
},

mouseResizeメソッドを追加して、ブラウザ上で左側の四角いボタンをクリックするとマウスの動きに合わせてタスクバーの横幅のサイズが変わります。

マウスの動きとタスクバーのサイズの拡大・縮小の同期を停止させるためにここでもmouseupイベントを利用します。mouseupイベントで実行する処理については既存のstopDragメソッドに追加を行います。


stopDrag(){
  if (this.dragging) {
    //mouseDownMoveの処理
  }
  if (this.leftResizing) {
    let diff = this.pageX - event.pageX;
    let days = Math.ceil(diff / this.block_size)
    if (days !== 0) {
      let task = this.tasks.find(task => task.id === this.task_id);
      let start_date = moment(task.start_date).add(-days, 'days')
      let end_date = moment(task.end_date)
      if (end_date.diff(start_date, 'days') <= 0) {
        task['start_date'] = end_date.format('YYYY-MM-DD')
      } else {
        task['start_date'] = start_date.format('YYYY-MM-DD')
      }
    } else {
      this.element.style.width = this.width;
      this.element.style.left = `${this.left.replace('px', '')}px`;
    }
  }
  this.dragging = false;
  this.leftResizing = false;
}

クリックした時の位置とクリックを外した時の位置の差分を出してblock_sizeでわり、何日分の拡大・縮小があったかを計算します。差の日数がある場合は、start_dateに差の日数を加えます。拡大する場合はそのまま日数を足すことになりますが、開始日が完了期限日より大きくなることはないのでその場合はstart_dateとend_dateを同じ日に設定します。もし拡大・縮小が小さい場合は保存したおいたクリック時のleftとwidthを設定しています。最後にleftResizingをfalseにしてマウスの動きとの同期を解除しています。

ブラウザで確認すると左側の四角いボタンをクリックしてマウスを左右に移動し、クリックを外すとタスクバーが拡大または縮小し、開始日が更新されるのを確認できます。

左側の四角での拡大・縮小が確認できたので、右側の四角も同様に設定を行います。

mouseResizeメソッドに右側の処理を追加します。右側の処理ではwidthのみ更新を行います。


mouseResize() {
  if (this.leftResizing) {
    let diff = this.pageX - event.pageX
    if (parseInt(this.width.replace('px', '')) + diff > this.block_size) {
      this.element.style.width = `${parseInt(this.width.replace('px', '')) + diff}px`
      this.element.style.left = `${this.left.replace('px', '') - diff}px`;
    }
  }
  if (this.rightResizing) {
    let diff = this.pageX - event.pageX;
    if (parseInt(this.width.replace('px', '')) - diff > this.block_size) {
      this.element.style.width = `${parseInt(this.width.replace('px', '')) - diff}px`
    }
  }
},

stopDragにも右側の処理を追加します。


stopDrag(){
  if (this.dragging) {
      //mouseDownMoveの処理
  }
  if (this.leftResizing) {
   // 左側の四角の拡大・縮小に関する処理
  }
  if (this.rightResizing) {
    let diff = this.pageX - event.pageX;
    let days = Math.ceil(diff / this.block_size)
    if (days === 1) {
      this.element.style.width = `${parseInt(this.width.replace('px', ''))}px`;
    } else if (days <= 2) {
      days--;
      let task = this.tasks.find(task => task.id === this.task_id);
      let end_date = moment(task.end_date).add(-days, 'days')
      task['end_date'] = end_date.format('YYYY-MM-DD')
    } else {
      let task = this.tasks.find(task => task.id === this.task_id);
      let start_date = moment(task.start_date);
      let end_date = moment(task.end_date).add(-days, 'days')
      if (end_date.diff(start_date, 'days') < 0) {
        task['end_date'] = start_date.format('YYYY-MM-DD')
      } else {
        task['end_date'] = end_date.format('YYYY-MM-DD')
      }
    }
  }
  this.dragging = false;
  this.leftResizing = false;
  this.rightResizing = false;
}

設定後は右側の四角をdrag&dropしてもタスクバーの拡大または縮小を行うことができます。拡大、縮小に合わせて完了期限日が更新されることも確認できます。

右側のカレンダー領域で行うDrag&Drop機能の実装は完了です。

タスクの進捗率

タスクの進捗率をタスクバーで視覚的にわかるようにタスクバーの内側に進捗度の幅を持つ要素を追加します。

classバインディングを利用してtaskの進捗率によってwidthの幅を決め、percentageプロパティの値が100の場合の時のみrounteded-r-lg(ブロック要素の右端の上下に丸みをもたせる)を適用しています。


<div class="w-full h-full" style="pointer-events: none;">
  <div class="h-full bg-yellow-500 rounded-l-lg" 
      style="pointer-events: none;" 
      :style="`width:${bar.task.percentage}%`"
      :class="{'rounded-r-lg': bar.task.percentage === 100}"></div>

ブラウザで確認するとオレンジ色で現在のタスクの進捗率を確認することができます。テスト1は進捗度が100%なのでタスクバーはオレンジ色になっています。

タスクバーで進捗率を確認
タスクバーで進捗率を確認

タスクリストの並び替え

左側のタスク領域にあるタスクの並び替えをDrag&Dropを使って行えるように設定を行っていきます。タスク要素をDrag&Dropするために今回はマウスイベントではなくドラッグイベントを利用します。

draggable属性の設定

タスク要素にdraggable属性を設定することでドラッグ操作が行えるようになります。


<div v-for="(list, index) in displayTasks" 
      :key="index" 
      class="flex h-10 border-b"  
      draggable=true>
デフォルトではdraggableはfalseに設定されており、draggableが設定されていない要素をドラッグしようとしても何も起こりません。
fukidashi

dragstartイベントの設定

draggableを設定した要素にdragstartイベントを設定します。dragstartイベントが発火するとdragTaskメソッドを実行します。


<iv v-for="(task, index) in displayTasks" 
      :key="index" 
      class="flex h-10 border-b"
      @dragstart="dragTask" 
      draggable=true>

dragTaskメソッドを追加し、drag開始直後にdragTaskメソッドが実行されるか確認します。


dragTask() {
  console.log('drag')
},

タスクの要素をドラッグし、デベロッパーツールのコンソールにdragのメッセージが表示されれば正常に設定は行われています。

dragstart時にdragを開始した要素の情報を取得するために新たにデータプロパティtaskを追加します。


data(){
  return {
    //略
    task:'',

drastartイベントに設定したdragTaskの引数にtaskを設定します。


<div v-for="(task, index) in displayTasks" 
      :key="index" 
      class="flex h-10 border-b"
      @dragstart="dragTask(task)" 
      draggable=true>

dragTaskメソッドでは引数で渡されたtaskを保存します。


dragTask(dragTask) {
  this.task = dragTask
}

dragoverイベント

dragした要素の並び替えを行うためにdragoverイベントを利用します。dragoverイベントはdragしている要素がその上を通っている間イベントが発火されます。


<div v-for="(task, index) in displayTasks" 
      :key="index" 
      class="flex h-10 border-b"
      @dragstart="dragTask"
      @dragover="dragTaskOver(task)"
      draggable=true>

dragoverイベントにdragTaskOverメソッドを指定し、引数にdragoverイベントを発火した要素のtask(dragしている要素の下にある要素)を指定します。

dragTaskOverメソッドには下記のコードを記述します。


dragTaskOver(overTask) {
  let deleteIndex;
  let addIndex;
  if (this.task.cat !== 'category') {
    if (overTask.cat === 'category') {
      let updateTask = this.tasks.find(task => task.id === this.task.id)
      updateTask['category_id'] = overTask['id']
    } else {
      if (overTask.id !== this.task.id) {
        this.tasks.map((task, index) => { if (task.id === this.task.id) deleteIndex = index })
        this.tasks.map((task, index) => { if (task.id === overTask.id) addIndex = index })
        this.tasks.splice(deleteIndex, 1)
        this.task['category_id'] = overTask['category_id']
        this.tasks.splice(addIndex, 0, this.task)
      }
    }
  }
}

dragTaskOverメソッドの中ではdragした要素がカテゴリーの場合は何も処理を行いません。

v-forで展開されているタスクリストの中にはカテゴリーとタスクの2種類のタスクを含んでいます。
fukidashi

dragした要素ではなくdragoverされた要素がカテゴリーの場合(cat=category)は、dragした要素のcatプロパティの値にdragoverされた要素のカテゴリーIDを設定します。これによりタスクはドラッグ前に所属していたカテゴリーから別のカテゴリーへDrag&Dropで動的に変更することができます。

dragoverの要素がタスクの場合(cat=task)はタスクの入れ替えを行っています。dragした要素をタスクリストの元の場所から削除し、新たにdragoverした要素がいたタスクリストの場所に挿入することでタスクの入れ替えを行います。

最後にdragoverイベントにpreventを設定してください。


@dragover.prevent="dragTaskOver(task)"

これでタスクの並び替えのコードは完了です。

カテゴリー単位の開閉機能

カテゴリー毎に表示・非表示できるように開閉機能を実装します。

開閉はcategoriesプロパティが持つcollapasedプロパティの値とv-ifディレクティブを利用します。


categories: [
    {
        id: 1,
        name: 'テストA',
        collapsed: false,
    }, 
    {
        id: 2,
        name: 'テストB',
        collapsed: false,
    }, 
],

collpasedがfalseの場合は表示し、trueの場合は非表示となります。デフォルトではfalseが設定されています。

開閉を行うボタンについてはSVGを利用し、https://heroicons.com/からSVGの情報を取得します。

サイトにアクセスし、検索フォームにchevronと入力し、開閉に利用するSVG情報を取得します。

SVGを検索
SVGを検索

タスク領域のタスク一覧に以下のように更新します。


<template v-if="task.cat === 'category'">
  <div
    class="flex items-center font-bold w-full text-sm pl-2 flex justify-between items-center bg-teal-100">
    <span>{{task.name}}</span>
    <div class="pr-4">
      <span v-if="task.collapsed">
        <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
          stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
        </svg>
      </span>
      <span v-else>
        <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
          stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
        </svg>
      </span>
    </div>
  </div>
</template>

collapesedがデフォルトではfalseなのでカテゴリーの右側に下矢印が表示されます。

カテゴリーに下矢印が表示
カテゴリーに下矢印が表示

この下矢印をクリックすることでカテゴリーを表示・非表示に変更するためclickイベントを設定します。clickイベントにはtoggleCategoryメソッドを指定し、引数にはtaskのidを入れます。


<template v-if="task.cat === 'category'">
  <div
    class="flex items-center font-bold w-full text-sm pl-2 flex justify-between items-center bg-teal-100">
    <span>{{task.name}}</span>
    <div class="pr-4" @click="toggleCategory(task.id)">
      <span v-if="task.collapsed">

Vue.jsのmethodsに指定してtoggleCategoryを追加します。渡されたidを利用してカテゴリー情報を取り出し、そのカテゴリーのcollapsedの値をfalseであればtrue, trueであればfalseに設定します。


toggleCategory(task_id) {
  let category = this.categories.find(category => category.id === task_id)
  category['collapsed'] = !category['collapsed'];
},

設定が完了後に下矢印をクリックすると右矢印にアイコンが変わります。クリックすると下矢印、右矢印が交互に表示されます。

右矢印の表示
右矢印の表示

クリックで矢印の表示以外には何も変化がありません。最後にcomputedプロパティのlistsにcategory.collapsedがfalseの場合のみタスクリストに追加するように条件を追加します。


lists() {
  let lists = [];
  this.categories.map(category => {
    lists.push({ cat: 'category', ...category });
    this.tasks.map(task => {
      if (task.category_id === category.id && !category.collapsed) {
        lists.push({ cat: 'task', ...task })
      }
    })
  })
  return lists;
},

条件を追加後、矢印をクリックすることでアイコンの変更とカテゴリーの開閉が行えることが確認できます。

テストAにある下矢印をクリックします。

クリック前の状態
クリック前の状態

クリックすると下矢印から右矢印にアイコンが変わり、テストAのカテゴリーの下にあるタスクが非表示になります。再度アイコンをクリックすると非表示になっていたタスクが再表示されます。

クリック後の状態
クリック後の状態

すべてのカテゴリーを非表示することも可能です。

すべてのカテゴリーを非表示
すべてのカテゴリーを非表示

カテゴリーの開閉機能を実装することができました。