Skip to content

Instantly share code, notes, and snippets.

@skysan87
Last active June 20, 2022 05:19
Show Gist options
  • Save skysan87/e116273036839af363850e7735d4edd4 to your computer and use it in GitHub Desktop.
Save skysan87/e116273036839af363850e7735d4edd4 to your computer and use it in GitHub Desktop.
[Vue]ガントチャート
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@next"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.3/dayjs.min.js"></script>
<!-- tailwindcss v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<title>ガントチャート</title>
</head>
<body>
<div id="app">
<div id="gantt" class="flex">
<div id="gantt-chart-container" ref="calendar" class="overflow-auto h-screen w-screen select-none">
<div id="header" class="top-0 flex z-20 sticky" :style="`width: ${viewWidth}px;`">
<div class="border-b border-black bg-green-100 flex sticky left-0 top-0"
:style="`min-width: ${taskWidth}px;`">
<div class="py-2 h-full px-2 border-r border-black text-sm" style="width: 120px">タスク</div>
<div class="py-2 h-full text-center border-r border-black text-sm" style="width: 90px">開始日</div>
<div class="py-2 h-full text-center border-r border-black text-sm" style="width: 90px">期限日</div>
</div>
<template v-for="month of calendars" :key="month.title">
<div class="border-b border-black bg-white" :style="`min-width: ${blockWidth * month.days}px;`">
<!-- 年月 -->
<div class="border-b border-r border-black px-2">{{ month.title }}</div>
<!-- 日付 -->
<div class="flex">
<div v-for="day of month.days" :key="day.date" class="border-r border-black text-xs text-center"
:class="weekendColor(day.dayOfWeek)" :style="`min-width: ${blockWidth}px;`">
{{ day.date }}
</div>
</div>
<!-- 曜日 -->
<div class="flex">
<div v-for="day of month.days" :key="day.date" class="border-r border-black text-xs text-center"
:class="weekendColor(day.dayOfWeek)" :style="`min-width: ${blockWidth}px;`">
{{ day.dayOfWeek }}
</div>
</div>
</div>
</template>
</div>
<div id="contents" class="relative" :style="`width: ${viewWidth}px;`">
<!-- timeline -->
<div v-for="i of totalDays" :key="i" class="absolute bg-gray-200 h-full w-px"
:style="`left: ${i * blockWidth + taskWidth - 1}px;`">
</div>
<!-- today -->
<div v-if="todayPosition >= 0" class="absolute bg-red-300 h-full"
:style="`width: ${blockWidth - 1}px; left: ${todayPosition}px;`">
</div>
<!-- contents -->
<div v-for="task of taskRows" :key="task.id" class="h-10 border-b border-black flex">
<div class="bg-green-100 z-10 flex sticky left-0" :style="`min-width: ${taskWidth}px;`">
<div class="py-2 h-full px-2 border-r border-black text-sm" style="width: 120px">{{ task.name }}</div>
<div class="py-2 h-full text-center border-r border-black text-sm" style="width: 90px">{{ task.startDate}}</div>
<div class="py-2 h-full text-center border-r border-black text-sm" style="width: 90px">{{ task.endDate }}</div>
</div>
<div :style="task.style" class="h-10 flex py-2 will-change-transform"
@mousedown="onMouseDown_MoveStart($event, task)">
<div class="w-2 bg-yellow-200 rounded-l-lg cursor-col-resize"
@mousedown.stop="onMouseDown_ResizeStart($event, task, 'left')">
</div>
<div class="flex-1 bg-yellow-200 pointer-events-none"></div>
<div class="w-2 bg-yellow-200 rounded-r-lg cursor-col-resize"
@mousedown.stop="onMouseDown_ResizeStart($event, task, 'right')">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
const BLOCK_SIZE = 20
const TASK_WIDTH = 300
Vue.createApp({
data () {
const _today = dayjs()
return {
dragging: false,
leftResizing: false,
rightResizing: false,
target: {
pageX: 0,
element: null,
task: null
},
blockWidth: BLOCK_SIZE,
taskWidth: TASK_WIDTH,
viewWidth: 0,
contentWidth: 0,
totalDays: 0,
startMonth: _today.add(0, 'month').format('YYYY-MM'),
endMonth: _today.add(1, 'month').format('YYYY-MM'),
calendars: [],
tasks: []
}
},
mounted () {
this.initView()
this.makeTestData()
document.addEventListener('mousemove', this.onMouseDown_Moving)
document.addEventListener('mouseup', this.onMouseDown_MoveStop)
document.addEventListener('mousemove', this.onMouseDown_Resizing)
document.addEventListener('mouseup', this.onMouseDown_ResizeStop)
},
unmounted () {
document.removeEventListener('mousemove', this.onMouseDown_Moving)
document.removeEventListener('mouseup', this.onMouseDown_MoveStop)
document.removeEventListener('mousemove', this.onMouseDown_Resizing)
document.removeEventListener('mouseup', this.onMouseDown_ResizeStop)
},
computed: {
taskRows () {
const startMonth = dayjs(this.startMonth)
const endMonth = dayjs(this.endMonth)
return this.tasks.map((task) => {
const dateFrom = dayjs(task.startDate)
const dateTo = dayjs(task.endDate)
const between = dateTo.diff(dateFrom, 'day') + 1
const start = dateFrom.diff(startMonth, 'day')
const end = endMonth.diff(dateTo, 'day') + endMonth.daysInMonth()
const pos = {
x: start * BLOCK_SIZE,
width: BLOCK_SIZE * between
}
const style = {
width: `${pos.width}px`,
transform: `translateX(${pos.x}px)`
}
// 表示範囲外の日付を含む場合は表示しない
const isHidden = !(start >= 0 && end > 0)
if (isHidden) {
style.display = 'none'
}
return {
style,
pos,
isHidden,
...task,
}
})
},
todayPosition () {
const today = dayjs()
const startDate = dayjs(this.startMonth)
const endDate = dayjs(this.endMonth)
const diffFuture = today.diff(startDate, 'day')
const diffPast = endDate.diff(today, 'day') + endDate.daysInMonth()
return (diffFuture >= 0 && diffPast > 0)
? diffFuture * BLOCK_SIZE + TASK_WIDTH
: -1
}
},
methods: {
initView () {
this.serCalendar()
this.totalDays = this.calendars.reduce((p, c) => p + c.days.length, 0)
this.contentWidth = this.totalDays * this.blockWidth
this.viewWidth = this.taskWidth + this.contentWidth
this.$nextTick(() => {
this.$refs.calendar.scrollLeft = this.todayPosition - TASK_WIDTH
})
},
getDays (startMonth) {
const dayOfWeek = ['日', '月', '火', '水', '木', '金', '土']
const days = []
for (let i = 0; i < startMonth.daysInMonth(); i++) {
const targetDate = startMonth.add(i, 'day')
days.push({
date: targetDate.date(),
dayOfWeek: dayOfWeek[targetDate.day()]
})
}
return days
},
serCalendar () {
const startMonth = dayjs(this.startMonth)
const endMonth = dayjs(this.endMonth)
const betweenMonth = endMonth.diff(startMonth, 'month')
for (let i = 0; i <= betweenMonth; i++) {
const targetMonth = startMonth.add(i, 'month')
this.calendars.push({
title: targetMonth.format('YYYY-MM'),
days: this.getDays(targetMonth)
})
}
},
onMouseDown_MoveStart (e, task) {
this.dragging = true
this.target.pageX = e.pageX
this.target.element = e.target
this.target.task = task
},
onMouseDown_Moving (e) {
if (!this.dragging) return
const realX = this.calcMovePositionX(e.pageX)
this.target.element.style.transform = `translateX(${realX}px)`
},
onMouseDown_MoveStop (e) {
if (!this.dragging) return
const realX = this.calcMovePositionX(e.pageX)
// 日付線にフィットさせる
const days = Math.round((this.target.task.pos.x - realX) / BLOCK_SIZE)
if (days !== 0) {
const task = this.tasks.find(task => task.id === this.target.task.id)
task['startDate'] = dayjs(task.startDate).add(-days, 'day').format('YYYY-MM-DD')
task['endDate'] = dayjs(task.endDate).add(-days, 'day').format('YYYY-MM-DD')
} else {
this.target.element.style.transform = `translateX(${this.target.task.pos.x}px)`
}
this.dragging = false
this.target.element = null
this.target.task = null
this.target.pageX = 0
},
onMouseDown_ResizeStart (e, task, direction) {
if (direction === 'left') {
this.leftResizing = true
} else {
this.rightResizing = true
}
this.target.pageX = e.pageX
this.target.element = e.target.parentElement
this.target.task = task
},
onMouseDown_Resizing (e) {
if (this.leftResizing) {
const realX = this.calcResizePositionX(e.pageX)
const realWidth = this.calcLeftResizeWidth(e.pageX)
this.target.element.style.transform = `translateX(${realX}px)`
this.target.element.style.width = `${realWidth}px`
}
if (this.rightResizing) {
const realWidth = this.calcRightResizeWidth(e.pageX)
this.target.element.style.width = `${realWidth}px`
}
},
onMouseDown_ResizeStop (e) {
if (this.leftResizing) {
const realX = this.calcResizePositionX(e.pageX)
// 日付線にフィットさせる
const days = Math.round((this.target.task.pos.x - realX) / BLOCK_SIZE)
if (days !== 0) {
const task = this.tasks.find(task => task.id === this.target.task.id)
task['startDate'] = dayjs(task.startDate).add(-days, 'day').format('YYYY-MM-DD')
} else {
this.target.element.style.transform = `translateX(${this.target.task.pos.x}px)`
this.target.element.style.width = `${this.target.task.pos.width}px`
}
}
if (this.rightResizing) {
const realWidth = this.calcRightResizeWidth(e.pageX)
// 日付線にフィットさせる
const days = Math.round((this.target.task.pos.width - realWidth) / BLOCK_SIZE)
if (days !== 0) {
const task = this.tasks.find(task => task.id === this.target.task.id)
task['endDate'] = dayjs(task.endDate).add(-days, 'day').format('YYYY-MM-DD')
} else {
this.target.element.style.width = `${this.target.task.pos.width}px`
}
}
this.leftResizing = false
this.rightResizing = false
this.target.element = null
this.target.task = null
this.target.pageX = 0
},
calcMovePositionX (currentPageX) {
const diff = this.target.pageX - currentPageX
return this.keepThreshold(
this.target.task.pos.x - diff
, 0
, this.contentWidth - this.target.task.pos.width
)
},
calcResizePositionX (currentPageX) {
const diff = this.target.pageX - currentPageX
return this.keepThreshold(
this.target.task.pos.x - diff
, 0
, this.target.task.pos.x + this.target.task.pos.width - BLOCK_SIZE
)
},
calcLeftResizeWidth (currentPageX) {
const diff = this.target.pageX - currentPageX
return this.keepThreshold(
this.target.task.pos.width + diff
, BLOCK_SIZE
, this.target.task.pos.width + this.target.task.pos.x
)
},
calcRightResizeWidth (currentPageX) {
const diff = this.target.pageX - currentPageX
return this.keepThreshold(
this.target.task.pos.width - diff
, BLOCK_SIZE
, this.contentWidth - this.target.task.pos.x
)
},
keepThreshold (value, min, max) {
if (value <= min) return min
if (value >= max) return max
return value
},
weekendColor (dayOfWeek) {
switch (dayOfWeek) {
case '土':
return 'bg-blue-100'
case '日':
return 'bg-red-100'
default:
return ''
}
},
makeTestData () {
const today = dayjs()
for (let i = 1; i <= 100; i++) {
this.tasks.push({
id: i,
name: `task - ${i}`,
startDate: today.format('YYYY-MM-DD'),
endDate: today.add(Math.floor(Math.random() * 5), 'day').format('YYYY-MM-DD')
})
}
}
}
}).mount('#app')
</script>
</html>
@skysan87
Copy link
Author

DEMO

demo.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment