本文書は、Googleカレンダーやネット上のライブラリを利用することなくJavaScriptで自作のカレンダー・スケジュールアプリケーションを作成してみたいという人のための記事です。本文書を読み進める前に以前公開した”本当にこれだけ?JavaScript+vue.jsでカレンダー作成”の続きなのでまずは下記の前回の記事を参考にカレンダーの外枠の作成を行ってください。

前回の記事通りに進めていくと下記のカレンダーをブラウザ上に描写することができるようになります。”前の月”、”次の月”ボタンを押すことで他の月のカレンダーを表示することができますがカレンダーの外枠だけでカレンダー上へのイベント表示やイベントの入力は行うことができません。

ボタンクリックで別の月に移動
作成したカレンダー

カレンダーの外枠では自作のカレンダーを作成したことにはなりません。カレンダーの外枠に加えて今回の記事ではカレンダーの必須機能である2つの機能を実装を行っていきます。

  • カレンダーにイベントを表示することができる
  • カレンダーのイベントを移動することができる

ドラッグ&ドロップ機能を備えた下記のような動作を行うことができます。

作成後の画面
作成後の画面

前回作成したコード

前回の記事で作成したCalendar.vueファイルの中身は下記の通りです。


<template>
  <h2>カレンダー{{ currentDate }}</h2>
  <button @click="prevMonth">前の月</button>
  <button @click="nextMonth">次の月</button>
  <div style="max-width:900px;border-top:1px solid gray;">
    <div
      v-for="(week, index) in calendars"
      :key="index"
      style="display:flex;border-left:1px solid gray"
    >
      <div
        v-for="(day, index) in week"
        :key="index"
        style="
            flex:1;min-height:125px;
            border-right:1px solid gray;
            border-bottom:1px solid gray
          "
      >
        {{ day.day }}
      </div>
    </div>
  </div>
</template>
<script>
import moment from "moment";
export default {
  data() {
    return {
      currentDate: moment(),
    };
  },
  methods: {
    getStartDate() {
      let date = moment(this.currentDate);
      date.startOf("month");
      const youbiNum = date.day();
      return date.subtract(youbiNum, "days");
    },
    getEndDate() {
      let date = moment(this.currentDate);
      date.endOf("month");
      const youbiNum = date.day();
      return date.add(6 - youbiNum, "days");
    },
    getCalendar() {
      let startDate = this.getStartDate();
      const endDate = this.getEndDate();
      const weekNumber = Math.ceil(endDate.diff(startDate, "days") / 7);

      let calendars = [];
      for (let week = 0; week < weekNumber; week++) {
        let weekRow = [];
        for (let day = 0;  day < 7; day++) {
          weekRow.push({
            day: startDate.get("date"),
          });
          startDate.add(1, "days");
        }
        calendars.push(weekRow);
      }
      return calendars;
    },
    nextMonth() {
      this.currentDate = moment(this.currentDate).add(1, "month");
    },
    prevMonth() {
      this.currentDate = moment(this.currentDate).subtract(1, "month");
    },
  },
  computed: {
    calendars() {
      return this.getCalendar();
    },
  },
}
</script>

カレンダーの表示設定

カレンダーの表示の見栄えをよくするためにclass、曜日表示設定など細かな変更を行います。

classの設定

CSSのclassの適用は必須ではありませんが、styleだとコードが少しわかりにくくなっているのでstyleからclassへと変更を行います。


<template>
  <div class="content">
    <h2>カレンダー{{ currentDate }}</h2>
    <div class="button-area">
      <button @click="prevMonth" class="button">前の月</button>
      <button @click="nextMonth" class="button">次の月</button>
    </div>
    <div class="calendar">
      <div 
        class="calendar-weekly"
        v-for="(week, index) in calendars"
        :key="index"
      >
        <div
          class="calendar-daily"
          v-for="(day, index) in week"
          :key="index"
        >
          <div class="calendar-day">
            {{ day.day }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

calanedar.vueファイルの下部にstyleタグを追加し、templateタグ内で設定したclassを追加します。


<style>
.content{
  margin:2em auto;
  width:900px;
}
.button-area{
  margin:0.5em 0;
}
.button{
  padding:4px 8px;
  margin-right:8px;
}
.calendar{
  max-width:900px;
  border-top:1px solid #E0E0E0;
  font-size:0.8em;
}
.calendar-weekly{
  display:flex;
  border-left:1px solid #E0E0E0;
  /* background-color: black; */
}
.calendar-daily{
  flex:1;
  min-height:125px;
  border-right:1px solid #E0E0E0;
  border-bottom:1px solid #E0E0E0;
  margin-right:-1px;
}
.calendar-day{
  text-align: center;
}
</style>

変化はほとんどありませんが、class適用後のカレンダーは下記のように表示されます。

class適用後のカレンダー
class適用後のカレンダー

タイトルの日付表示変更

カレンダーの上部に表示されている日付の表示を20XX年X月へと変更します。表示内容を変更するためにcomputedプロパティdisplayDateを追加します。moment.jsで作成したcurrentDateにformat関数を使って表示設定を行っています。


computed: {
  //略
  displayDate(){
    return this.currentDate.format('YYYY[年]M[月]')
  },
},

templateタグ内で行っていた日時表示をcurrentDateからdisplayMonthに変更します。


<h2>カレンダー {{ displayMonth }}</h2>

2021年1月と表示が変わります。実行した日によって表示される日付は変わります。

日付表示の変更
日付表示の変更

曜日を表示

カレンダーには日しか表示されていないのでカレンダーの上部に曜日が表示できるよう設定を行います。

そのまま曜日をtemplateタグ内に記述することもできますが曜日の数字から日本語の曜日に変更を行うyoubiメソッドを追加します。


youbi(dayIndex) {
  const week = ["日", "月", "火", "水", "木", "金", "土"];
  return week[dayIndex];
},

v-forを利用して7を1,2,..,7と展開してyoubiメソッドを実行して曜日を表示させます。v-forの展開では1から始まるのでyoubiの引数で-1しています。


<div class="calendar">
  <div class="calendar-weekly">
    <div class="calendar-youbi" v-for="n in 7" :key="n">
      {{ youbi(n-1) }}
    </div>
  </div>
  <div
    class="calendar-weekly"
    v-for="(week, index) in calendars"
    :key="index"
  >

div要素にcalendar-youbiクラスも設定しているのでstyleタグ内にcalendar-youbiを追加します。


.calendar-youbi{
  flex:1;
  border-right:1px solid #E0E0E0;
  margin-right:-1px;
  text-align:center;
}

ブラウザで確認するとカレンダーの先頭に曜日が表示されます。

曜日の表示
曜日の表示

表示月以外の日付枠の表示変更

カレンダーには表示年月以外の日付がカレンダーの最初と最後に表示されます。表示年月と表示年月以外の日を区別するために表示年月以外の日の背景色を設定します。

背景色を分けるために条件式を利用しますがその中で使うcomputedプロパティcurrentMonthを追加します。


currentMonth(){
  return this.currentDate.format('YYYY-MM')
},

表示させている年月はcurrentMonthによってわかります。

templateタグの中で展開しているcalendarsの情報に日数だけではなく年月の情報も持たせます。


let calendars = [];
let calendarDate = this.getStartDate();

for (let week = 0; week < weekNumber; week++) {
  let weekRow = [];
  for (let day = 0;  day < 7; day++) {
    weekRow.push({
      day: calendarDate.get("date"),
      month: calendarDate.format("YYYY-MM"),//追加
    });
    calendarDate.add(1, "days");
  }
  calendars.push(weekRow);
}
前回までのコードではstartDateを利用していましたが本文書ではカレンダーの情報を作成するループの中ではcalendarDateを利用するためcalendarDateを追加しています。

classバインディングを利用してcurrnetMonthの値とday.monthの値が異なる場合のみclassのoutsideを適用します。


<div
  class="calendar-daily"
  :class="{outside: currentMonth !== day.month}" //追加
  v-for="(day, index) in week"
  :key="index"
>
  <div class="calendar-day">
    {{ day.day }}
  </div>
</div>

styleタグにoutsideクラスを追加します。


.outside{
  background-color: #f7f7f7;
}

ブラウザを使って表示年月以外の日付の背景色があることが確認できます。

表示月と異なる日付に背景色
表示月と異なる日付に背景色

イベントの表示

ここからカレンダー上にイベントを表示させる機能を実装していきます。

イベント情報の準備

データプロパティeventsを追加してid、name, start, end, colorを持つオブジェクト20件を追加します。startはイベントの開始日、endはイベントの終了日です。colorは表示させるイベント要素の背景色に利用します。


events:[
  { id: 1, name: "ミーティング", start: "2021-01-01", end:"2021-01-01", color:"blue"},
  { id: 2, name: "イベント", start: "2021-01-02", end:"2021-01-03", color:"limegreen"},
  { id: 3, name: "会議", start: "2021-01-06", end:"2021-01-06", color:"deepskyblue"},
  { id: 4, name: "有給", start: "2021-01-08", end:"2021-01-08", color:"dimgray"},
  { id: 5, name: "海外旅行", start: "2021-01-08", end:"2021-01-11", color:"navy"},
  { id: 6, name: "誕生日", start: "2021-01-16", end:"2021-01-16", color:"orange"},
  { id: 7, name: "イベント", start: "2021-01-12", end:"2021-01-15", color:"limegreen"},
  { id: 8, name: "出張", start: "2021-01-12", end:"2021-01-13", color:"teal"},
  { id: 9, name: "客先訪問", start: "2021-01-14", end:"2021-01-14", color:"red"},
  { id: 10, name: "パーティ", start: "2021-01-15", end:"2021-01-15", color:"royalblue"},
  { id: 12, name: "ミーティング", start: "2021-01-18", end:"2021-01-19", color:"blue"},
  { id: 13, name: "イベント", start: "2021-01-21", end:"2021-01-21", color:"limegreen"},
  { id: 14, name: "有給", start: "2021-01-20", end:"2021-01-20", color:"dimgray"},
  { id: 15, name: "イベント", start: "2021-01-25", end:"2021-01-28", color:"limegreen"},
  { id: 16, name: "会議", start: "2021-01-21", end:"2021-01-21", color:"deepskyblue"},
  { id: 17, name: "旅行", start: "2021-01-23", end:"2021-01-24", color:"navy"},
  { id: 18, name: "ミーティング", start: "2021-01-28", end:"2021-01-28", color:"blue"},
  { id: 19, name: "会議", start: "2021-01-12", end:"2021-01-12", color:"deepskyblue"},
  { id: 20, name: "誕生日", start: "2021-01-30", end:"2021-01-30", color:"orange"},
]
ここでは直接eventsを記述していますが、通常はイベント情報はデータベース等に保存されているのでそれらのデータソースから取得する必要があります。また本書を元に動作確認を行う場合は日付の2021-01を実施する日付に変更して実施してください。2021年の7月であれば2021-07と一括で変更してください。

各日付にイベントを表示させるためには各日にどのイベントが含まれるかをチェックする仕組みが必要となります。

forループの中でgetDayEventsメソッド(次の章でコードは作成)を追加し、getDayEventsメソッドの中でcalendarDateの日に開催されているイベントをすべて取得し、dayEventsの変数に保存します。変数dayEventsはday, monthと一緒にweekRowの中にオブジェクトとして保存します。


for (let week = 0; week < weekNumber; week++) {
  let weekRow = [];
  for (let day = 0;  day < 7; day++) {
    let dayEvents = this.getDayEvents(calendarDate)
    weekRow.push({
      day: calendarDate.get("date"),
      month: calendarDate.format("YYYY-MM"),
      dayEvents
    });
    calendarDate.add(1, "days");
  }
  calendars.push(weekRow);
}

各日のイベントを取得

各日のイベントをeventsプロパティから取得するためにgetDayEventsメソッドを追加します。getDayEventsはfilter関数を使ってeventsを展開します。filter関数の中では各イベントの開始日と終了日の間にイベントを取得したい日が含まれているかチェックを行っています。getDayEventsを実行するとgetDayEventsの引数に入った日付dateが含まれるイベントが取得できます。


getDayEvents(date){
  return this.events.filter(event => {
    let startDate = moment(event.start).format('YYYY-MM-DD')
    let endDate = moment(event.end).format('YYYY-MM-DD')
    let Date = date.format('YYYY-MM-DD')
    if(startDate <= Date && endDate >= Date) return true;
  });
}

イベントを表示

getDayEVentsで取得したイベントdayEventsはtemplateタグの中で展開し、styleバインディングを利用して背景色の設定を行いイベント名を表示しています。展開後のdayEventオブジェクトにはid, color, name, start, endプロパティが含まれています。


<div
  class="calendar-daily"
  :class="{outside: currentMonth !== day.month}"
  v-for="(day, index) in week"
  :key="index"
>
  <div class="calendar-day">
    {{ day.day }}
  </div>
  <div v-for="dayEvent in day.dayEvents" :key="dayEvent.id" >
    <div 
      class="calendar-event"
      :style="`background-color:${dayEvent.color}`" >
      {{ dayEvent.name }}
    </div>
  </div>
</div>

calendar-eventクラスをstyleタグに追加します。


.calendar-event{
  color:white;
  margin-bottom:1px;
  height:25px;
  line-height:25px;
}

ここまでのコードの追加でブラウザを確認するとブラウザ上のカレンダーにデータプロパティeventsに登録されたすべてのイベントが表示されます。

カレンダーにイベントを表示
カレンダーにイベントを表示

データプロパティeventsで設定したイベントは開始日と終了日、その期間も含めてすべて表示されています。すべてのイベント情報が表示されたのでこれで終了といいたいところですがここからさらにイベントの表示について変更を加えていきます。

変更により複数日にまたがるイベントを1つのイベントとして表示させます。例えば1月8日、9日に海外旅行のイベントが入っていますが、実際には1つのイベントですがここだけ見ると2つのイベントが別イベントとして登録されているように見えます。

複数日にまたがるイベント
複数日にまたがるイベント

下記のように複数日にまたがるイベントは1つのイベントとして表示させるように変更を行います。

複数日にまたがるイベントを1つのイベントに
複数日にまたがるイベントを1つのイベントに

表示の問題だけではなく一つのイベントと設定することでドラッグ&ドロップを実行した際も一つの要素として移動することができます。

ドラッグ&ドロップの実装は本文書の後半で実装します。

ドラッグ&ドロップを行うためにイベントの要素にdraggable属性を設定します。


<div 
  class="calendar-event"
  :style="`background-color:${dayEvent.color}`"
  draggable="true" > //追加
  {{ dayEvent.name }}
</div>

複数日にまたがるイベントを1つのイベントとして設定した場合には下記のように1つの要素(イベント)として移動できます。

Drap&Dropを1つの要素
Drap&Dropを1つの要素

現在の設定では複数日にまたがるイベントにも関わらず各日のイベントが個別の要素として表示されているので複数日にまたがったイベントも1つ1つ別の要素としてドラッグ&ドロップすることになります。

個別のDrag&Drop
個別のDrag&Drop

各イベントを一つの要素として表示

各イベントを1つの要素と表示するためにはイベントの日数によって要素の幅を変更する必要があります。

後ほどdayEventsに入るeventオブジェクトに幅の情報を追加するためgetDayEventsメソッド内で利用していたfilter関数をforEach関数に変更を行い、開始日がイベントを取得したい日に一致するイベントのみ取り出します。


getDayEvents(date){
  let dayEvents = [];
  this.events.forEach(event => {
    let startDate = moment(event.start).format('YYYY-MM-DD')
    let Date = date.format('YYYY-MM-DD')
    if(startDate == Date){ //開始日のみに制限
      dayEvents.push(event)
    }
  });
  return dayEvents;
}

開始日のみに制限したので各イベントの開始日のみブラウザ上に表示されます。

イベントの開始日のみから抽出したイベント
イベントの開始日のみから抽出したイベント

ブラウザ上に表示する各イベントの要素は日数に応じてwidthを変更します。startDateとendDateを使って間の日数を取得し、取得後は日数に100をかけてwidthを設定します。複数の日数がない場合はwidthは95とします。95がwidthに設定した場合はstyle=”width:95%”を設定するので日付の枠の95%の幅でイベントの要素が表示されます。


getDayEvents(date){
  let dayEvents = [];
  this.events.forEach(event =< {
    let startDate = moment(event.start).format('YYYY-MM-DD')
    let endDate = moment(event.end).format('YYYY-MM-DD')
    let Date = date.format('YYYY-MM-DD')

    if(startDate == Date){
      let betweenDays = moment(endDate).diff(moment(startDate), "days")
      let width = betweenDays * 100 + 95;

      dayEvents.push({...event,width})
    }
  });
  return dayEvents;
}

取得したwidthはtemplateタグのイベントの展開時にstyleを使って設定を行います。


<div 
  class="calendar-event"
  :style="`width:${dayEvent.width}%;background-color:${dayEvent.color}`" 
  draggable="true" >
  {{ dayEvent.name }}
</div>

ブラウザで確認すると各イベントの幅を設定することができましたが一部のイベントがカレンダーから飛び出してしまいます。

幅が設定されたイベント
幅が設定されたイベント

カレンダーの横幅を超えたイベントの日数は下の行の次の週に表示させる必要があります。1月8日の開始日である海外旅行のイベントは1/8, 1/9と次の週の1/10,1/11に表示されるように設定を行います。

カレンダーの幅を超えないように制御するためにgetEventWidthメソッドを追加します。getEventWidthでは開始日と終了日だけではなくday(曜日)も利用します。1/8の海外旅行の場合はbetweenDaysが4日で金曜が開始日なのでdayの曜日番号は5になり、widthは195となります。


getEventWidth(end, start, day){
  let betweenDays = moment(end).diff(moment(start), "days")
  if(betweenDays > 6 - day){
    return (6 - day) * 100 + 95; 
  }else{
    return betweenDays * 100 + 95;
  }
},

getEventWidthメソッドに曜日番号dayが必要となるため、getDayEventsにも引数dayを追加する必要があります。


getDayEvents(date, day){
//略
    if(startDate === Date){
      let width = this.getEventWidth(startDate, endDate, day)
      dayEvents.push({...event,width})
    }
  });
  return dayEvents;
},

geeDayEventsを呼び出しているgetCalendarメソッド内のgetDayEventsの引数にもdayを追加します。


getCalender(){
//略
  let dayEvents = this.getDayEvents(calendarDate, day);
//略
}

上記の設定により曜日によってイベントの幅が制御できるようになったためカレンダーの幅からイベントが飛び出すことはなくなります。しかし週をまたがったイベントについては途中でイベントが終了していることもわかります。

イベントの長さはカレンダーの幅におさまる
イベントの長さはカレンダーの幅におさまる

次はカレンダーの横幅を超えたイベントの日数を下の行の次の週の週初めから表示させる設定を行います。

先ほどは開始日のみ注目していましが、開始日だけではなくイベントに含まれる日に週の開始曜日である日曜日が含まれているのかチェックを行います。開始日ではなく日曜日が含まれているイベントの場合はイベントが週をまたがっているので日曜日から残りのイベントの幅を計算してwidthを設定します。


if(startDate <= Date && endDate >= Date){
  if(startDate === Date){
    let width = this.getEventWidth(startDate, endDate, day)
    dayEvents.push({...event,width})
  }else if(day === 0){
    let width = this.getEventWidth(date, endDate, day)
    dayEvents.push({...event,width})
  }
}
イベントが週をまたがった場合の表示
イベントが週をまたがった場合の表示

イベントの開催期間に日曜日が含まれるかどうかチェックすることで週をまたがったイベントも正しく日数分表示できることが確認できます。

表示されるイベントが少ない場合は気づかないかもしれませんが、ここまでのコードでは一部イベントの重なりの問題が発生していることが確認できます。

カレンダー上でのイベントの重なり
カレンダー上でのイベントの重なり

12日から開始されたイベントは12日から15日ですが、14日は客先訪問と重なり、15日にはパーティと重なっており正しく表示されていません。

正しくは下記のように表示させる必要があります。

イベントの重なりがない正しい表示
イベントの重なりがない正しい表示

イベントの重なりの解消

カレンダーにイベントを表示させる上で最も重要な箇所がこの部分です。説明するのも難しく説明もわかりにくいですがカレンダーのイベントを正しく表示させるために頑張って読み進めてください。

これまでの設定では各イベントは独立しており、表示する際に他のイベントからの影響は全く考えていません。しかし、重なりを解消するためには同じ日(開始日が異なる)に開催されているイベントとの関係を加味する必要があります。

例えば14日の客先訪問を表示させる場合は、14日よりも以前にイベント(12日から15日)が開始されていることを客先訪問のイベントを表示する際に知っておく必要があります。先に開始したイベントが先に表示領域を確保できるため最初にeventsを日付で並び替えておきます。

イベントの並び替え

computedプロパティにsortedEventsを追加します。開始日でソートを行っています。ソートを行うと開始日順にイベントが並び替えられることになります。

データベースから取得時にソートして取り出している場合はこの処理は必要ありません。

sortedEvents(){
  return this.events.slice().sort(function(a,b) {
    let startDate = moment(a.start).format('YYYY-MM-DD')
    let startDate_2 = moment(b.start).format('YYYY-MM-DD')
    if( startDate < startDate_2 ) return -1;
    if( startDate > startDate_2 ) return 1;
    return 0;
  })
}

getDayEventsの中ではeventsでforEachを実行していましたがソートしたsortedEventsに変更します。


getDayEvents(date, day){
  let dayEvents = [];
  this.sortedEvents.forEach(event => {
//略
  });
  return dayEvents;
},

eventsからsortedEventsに変更しただけではブラウザに表示される内容に変化はありません。

開始済みイベントの保存

イベント間の関係を追加するために新たにgetDayEventsメソッドの中にstackIndexとstartedEventsの変数を2つ追加します。

同じ日にイベントがある場合はカレンダー上の1日の枠に複数のイベントが積み重なって表示されます。stackIndexを使って一日に含まれる複数のイベントが重ならないように一意の番号を各イベントに保持させます。1日に同じstackIndexを持つイベントは有りません。startedEventsはイベントの積み重ねを調べたい日ですでに開始されているイベントを保存します。startedEventsに保存されるイベントはstackIndexを持っています。startedEventsが利用していないstackIndexがその日開始される新しいイベントに付与されます。

stackIndexをチェックすることで開始されているイベントがすでにどの場所に表示されているのかがわかるため新規のイベントには同じ位置に表示させないように制御します。

これまでは開始日とイベントに日曜が含まれるかどうかのチェックをおこなっていましたが、それ以外にどちらにも当てはまらないイベントをstartedEventsに保存します。


getDayEvents(date, day){
  let stackIndex = 0; //追加
  let dayEvents = [];
  let startedEvents = []; //追加
  this.sortedEvents.forEach(event => {
    let startDate = moment(event.start).format('YYYY-MM-DD')
    let endDate = moment(event.end).format('YYYY-MM-DD')
    let Date = date.format('YYYY-MM-DD')
    if(startDate <= Date && endDate >= Date){
      if(startDate === Date){
      let width = this.getEventWidth(startDate, endDate, day)
      dayEvents.push({...event,width})
      }else if(day === 0){
        //略
      }else{
        //追加処理
      }
    }
  });
  return dayEvents;
},

ここまでのコードでは他のイベントの影響を考慮していないため、開始日または日曜日を含むイベントはwidthの計算以外に何の追加処理なくdayEventsに保存していました。ここからは開催期間が重複している場合に他のイベントの影響を加える処理を追加していきます。

他のイベントの影響を加える処理

他のイベントの影響を加味するため新たにgetStackEventsメソッドを追加します。getStackEventsメソッドの中ではさらにgetStartedEventsメソッドを実行してます。

getStartedEventsメソッドでは、すでに開始されているイベントstartedEventsのstackIndexと新たにdayEventsに追加したいイベントのstackIndexが重複しないかチェックを行っています。もし重複する場合は新規のイベントには別のstackIndexを付与しstackIndexが絶対に重複しないようにしています。(ソートにより開始日順にイベントが並んでいるため新規のイベントより先にstartedEventsの中に開始済イベントが保存されています。)他のイベントとの重複がないイベントはstackIndexを取得後はeventオブジェクトにその値を保存してdayEventsに追加されます。getStartedEventsの中ではstackIndexのチェックだけではなくすでに開始済のイベントも後ほどダミー領域として利用するためあらかじめ保持している現在のstackIndexと同じ値を持っている場合はstackIndexとともにdayEventsに追加しておきます。そのstackIndexは新規のイベントには利用でいないのでstackIndexを1増やして重複しないようにしています。ダミー領域の場合はwidthを設定せずにdayEventsに追加していますが新規のイベントを追加する場合はwidthを検索した後にdayEventsに追加しています。


getStackEvents(event, day, stackIndex, dayEvents, startedEvents, start){
  [stackIndex, dayEvents] = this.getStartedEvents(stackIndex, startedEvents, dayEvents)
  let width = this.getEventWidth(start, event.end, day)
  Object.assign(event,{
    stackIndex
  })
  dayEvents.push({...event, width})
  stackIndex++;
  return [stackIndex,dayEvents];
},
getStartedEvents(stackIndex, startedEvents, dayEvents){
  let startedEvent;
  do{
    startedEvent = startedEvents.find(event => event.stackIndex === stackIndex)
    if(startedEvent) {
      dayEvents.push(startedEvent) //ダミー領域として利用するため
      stackIndex++;
    }
  }while(typeof startedEvent !== 'undefined')
  return [stackIndex, dayEvents]
},
do while構文ではstartedEventsの中にstackIndexを持つイベントがあるかチェックをしています。ある場合はstackIndexの値を1つ増やしstackIndexを持つイベントがなくなるまでループを回します。一致するイベントがない場合はすぐにdo whileを抜けます。

追加したgetStackEventsはgetDayEventsに追加します。


getDayEvents(date, day){
  let stackIndex = 0;
  let dayEvents = [];
  let startedEvents = [];
  this.sortedEvents.forEach(event => {
    let startDate = moment(event.start).format('YYYY-MM-DD')
    let endDate = moment(event.end).format('YYYY-MM-DD')
    let Date = date.format('YYYY-MM-DD')
    if(startDate <= Date && endDate >= Date){
      if(startDate === Date){
        [stackIndex, dayEvents] = this.getStackEvents(event, day, date, stackIndex, dayEvents, startedEvents, event.start);
      }else if(day === 0){
        [stackIndex, dayEvents] = this.getStackEvents(event, day, date, stackIndex, dayEvents, startedEvents, date);
      }else{
        startedEvents.push(event)
      }
    }
  });
  return dayEvents;
},

複数の日数を含むイベントが今日開始するイベントの下にある場合はその場所にダミーの領域を確保する必要があります。dayEventsの中にはすでに開始済みの情報startedEventの内容も保存されているのでその情報をダミー情報に利用します。v-ifディレクティブを利用してそのイベントにwidthが設定されていない場合はダミーの領域なので高さ26pxのダミー領域を確保します。


<div v-for="dayEvent in day.dayEvents" :key="dayEvent.id" >
  <div
    v-if="dayEvent.width"
    class="calendar-event"
    :style="`width:${dayEvent.width}%;background-color:${dayEvent.color}`" 
    draggable="true" >
    {{ dayEvent.name }}
  </div>
  <div v-else style="height:26px"></div>
</div>

ブラウザで確認するとイベントの重複が解消され以下のように表示されます。14日の客先訪問の下のグリーンの領域にはダミー領域が存在することで重なりが解消しています。

イベントの重複を解消
イベントの重複を解消

表示設定(ボーダー、イベント要素)

複数の日数をまたがるイベントが一つの要素として表示されましたが、日付のボーダーが表示されているのでボーダーよりも要素が上になるようにpostionのrelativeとindexを設定します。またborder-radiusとpaddingも設定しておきます。


.calendar-event{
  color:white;
  margin-bottom:1px;
  height:25px;
  line-height:25px;
  position: relative;
  z-index:1;
  border-radius:4px;
  padding-left:4px;
}

設定後は下記のように表示されます。

CSS適用後のイベントの表示
CSS適用後のイベントの表示

以上でイベントの表示設定は完了です。

ドラッグ&ドラッグ機能

表示されているイベントをDrag&Dropで別の日に移動できるように設定を行います。draggable属性をイベントの要素に設定ずみなのでイベントのドラッグを行うことはできます。

dragstartイベントの設定

ドラッグした要素のイベントのidを取得するためdragstartイベントをイベント要素に設定します。


<div
  v-if="dayEvent.width"
  class="calendar-event"
  :style="`width:${dayEvent.width}%;background-color:${dayEvent.color}`" 
  draggable="true"
  @dragstart="dragStart($event, dayEvent.id)">
  {{ dayEvent.name }}
</div>

dragStartメソッドを追加してイベントのIDが取得できるか確認します。


dragStart(event, eventId){
  console.log(eventId);
}

イベント要素をドラッグしてデベロッパーツールのコンソールにイベントのIDが表示されることを確認してください。

イベントのIDを保存するためにevent.dataTransferを利用します。event.dataTransfer.setDataで設定したIDはgetDataメソッドで取得することができます。


dragStart(evet, eventId){
  event.dataTransfer.effectAllowed = "move";
  event.dataTransfer.dropEffect = "move";
  event.dataTransfer.setData("eventId", eventId);
},

dropイベントの設定

日付の枠にドロップされたイベントを検知するためにdropイベントを設定します。


<div
  class="calendar-daily"
  :class="{outside: currentMonth !== day.month}"
  v-for="(day, index) in week"
  :key="index"
  @drop="dragEnd"
  @dragover.prevent
>
dragover.preventを設定しないとdropイベントが発火しないので忘れずに設定を行ってください。

dragEndイベントを設定します。event.dataTransfer.setDataで設定したイベントのIDをevent.dataTransfer.getDataで取得します。


dragEnd(){
  console.log(event.dataTransfer.getData("eventId"));
}

イベントをドラッグしてカレンダーの日付のどこかにドロップしてコンソールにイベントのIDが表示させることを確認してください。

開始日の変更

ドラッグ&ドロップ後にイベントの開始日と終了日をドロップした日付を利用して更新する必要があります。ドロップした日付が何日なのかをしるためには日付の情報が必要になります。

getCalenderメソッド内のweekRowで日付の情報も日、年月、イベントと一緒に保存します。


weekRow.push({
  day: calendarDate.get("date"),
  month: calendarDate.format("YYYY-MM"),
  date: calendarDate.format("YYYY-MM-DD"),
  dayEvents:dayEvents,
});

追加したdateをDropイベントのdragEndメソッドの引数に設定します。


@drop="dragEnd($event, day.date)"

dragEndメソッドで日付を取得できることを確認します。


dragEnd(event, date){
  console.log(event.dataTransfer.getData("eventId"));
  console.log(date);
}

ドロップした場所の日付が確認できたらイベントIDを利用してイベントの開始日と終了日を更新します。


dragEnd(event, date){
  let eventId = event.dataTransfer.getData("eventId");
  let dragEvent = this.events.find(event => event.id == eventId)
  let betweenDays = moment(dragEvent.end).diff(moment(dragEvent.start), "days");
  dragEvent.start = date;
  dragEvent.end = moment(dragEvent.start).add(betweenDays, "days").format("YYYY-MM-DD");
},

これでドラッグ&ドロップの設定は完了です。イベント要素をドラッグして別の場所にドロップできることを確認してください。

イベントをドラッグ&ドロップ
イベントをドラッグ&ドロップ