(() => { 'use strict' let _CALENDAR // レコード詳細/新規作成/編集画面表示時にカレンダーを初期化 kintone.events.on( ['app.record.detail.show', 'app.record.create.show', 'app.record.edit.show'], (event) => { kintone.app.record.setFieldShown('休日の生データ', false); initCalendarArea( event.record, getHolidayMap(event.record), event.type === 'app.record.detail.show' ) return event } ) // 年度フィールド変更時に各月の休日をクリアしてカレンダーを再描画 kintone.events.on( [ 'app.record.create.change.年度', 'app.record.edit.change.年度', 'app.record.index.edit.change.年度' ], (event) => { forEachRawFields((field) => { event.record[field].value = '' }) if (event.type !== 'app.record.index.edit.change.年度') { initCalendarArea(event.record) } return event } ) // 一覧編集画面では各月の休日フィールドを編集不可にする kintone.events.on( ['app.record.index.edit.show'], (event) => { forEachRawFields((field) => { event.record[field].disabled = true }) return event } ) // レコード保存時にカレンダーで選択した休日を各月フィールドへ反映 kintone.events.on( ['app.record.create.submit', 'app.record.edit.submit'], (event) => { const resultMap = _CALENDAR.getSelectedDatesMap() forEachRawFields((field, i) => { event.record[field].value = resultMap[i].join(',') }) return event } ) /* -------------------------------------------------- * 初期化・データ準備 * -------------------------------------------------- */ // カレンダー表示エリアを初期化 function initCalendarArea(record, map = {}, readonly = false) { const calendarAreaEl = kintone.app.record.getSpaceElement('calendar-area') calendarAreaEl.innerHTML = '' const year = record.年度.value if (isYearString(year)) { _CALENDAR = createCalendar(Number(year), map, readonly) calendarAreaEl.appendChild(_CALENDAR.fragment) } } // 既存レコードに保存されている休日を取得 function getHolidayMap(record) { const res = {} forEachRawFields((field, i) => { const str = record[field].value if (!str) { return } res[i] = new Set(str.split(',')) }) return res } /* -------------------------------------------------- * ユーティリティ * -------------------------------------------------- */ // 各月フィールドをループ const forEachRawFields = (callback) => { for (let i = 1; i <= 12; i++) { callback(`休日_${i}月`, i) } } // ゼロパディング const padZero = (n) => (n < 10 ? '0' + n : n) // Date → yyyy-MM-dd const toYmd = (d) => `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}` // 土日判定 const isWeekend = (d) => d.getDay() === 0 || d.getDay() === 6 // 年度文字列の妥当性チェック function isYearString(str) { // 文字列かつ空でない if (typeof str !== 'string' || str.length === 0) { return false } // 数字のみ if (!/^\d+$/.test(str)) { return false } // 4桁 if (str.length !== 4) { return false } // 1900-3000 の範囲 const year = parseInt(str, 10) return year >= 1900 && year <= 3000 } /* -------------------------------------------------- * カレンダー生成 * -------------------------------------------------- */ const createCalendar = (year, highlightMap, readonly) => { const fragment = document.createDocumentFragment() /* ----- 図例 ----- */ const legend = document.createElement('div') legend.id = 'legend' legend.innerHTML = '休日' fragment.appendChild(legend) /* ----- カレンダーコンテナ ----- */ const calendar = document.createElement('div') calendar.id = 'calendar' if (readonly) { calendar.classList.add('readonly') } fragment.appendChild(calendar) /* ----- 内部状態 ----- */ const selectedSet = new Set() const months = buildMonths(year) months.forEach((m) => { calendar.appendChild(buildMonth(m)) }) /* ---------- 月情報生成 ---------- */ function buildMonths(year) { return Array.from({ length: 12 }, (_, i) => { const monthIndex = 3 + i // 0 基準月で 4 月始まり const date = new Date(year, monthIndex, 1) return { displayMonth: date.getMonth() + 1, year: date.getFullYear(), month: date.getMonth(), label: `${date.getFullYear()}年${date.getMonth() + 1}月` } }) } /* ---------- 月単位カレンダー生成 ---------- */ function buildMonth({ displayMonth, year, month, label }) { const monthBox = document.createElement('div') monthBox.className = 'month' buildTitle(monthBox, label) buildHeader(monthBox) const first = new Date(year, month, 1) buildPadBlocksStart(first, monthBox) buildDateBlocks(year, month, displayMonth, monthBox) buildPadBlocksEnd(monthBox) return monthBox } function buildTitle(monthBox, label) { const title = document.createElement('div') title.className = 'title' title.textContent = label monthBox.appendChild(title) } function buildHeader(monthBox) { ['日', '月', '火', '水', '木', '金', '土'].forEach((day, i) => { const cell = document.createElement('div') cell.className = 'cell header' addWeekendClass(cell, i) cell.textContent = day monthBox.appendChild(cell) }) } function addWeekendClass(cell, week) { if (week === 0) { cell.classList.add('sun') } else if (week === 6) { cell.classList.add('sat') } } function buildPadBlocksStart(first, monthBox) { const startDayInWeek = first.getDay() for (let i = 0; i < startDayInWeek; i++) { const cell = document.createElement('div') cell.className = 'cell' monthBox.appendChild(cell) } } function buildDateBlocks(year, month, displayMonth, monthBox) { const last = new Date(year, month + 1, 0) const highlights = highlightMap[displayMonth] || null for (let day = 1; day <= last.getDate(); day++) { const dateObj = new Date(year, month, day) const ymd = toYmd(dateObj) const cell = document.createElement('div') cell.className = 'cell' cell.textContent = day cell.dataset.date = ymd addWeekendClass(cell, dateObj.getDay()) // 初期選択判定 let shouldSelect = false if (highlights === null) { shouldSelect = isWeekend(dateObj) } else if (highlights instanceof Set) { shouldSelect = highlights.has(ymd) } if (shouldSelect) { cell.classList.add('selected') selectedSet.add(ymd) } // 編集可モードのみクリックイベントを追加 if (!readonly) { cell.addEventListener('click', () => { cell.classList.toggle('selected') if (cell.classList.contains('selected')) { selectedSet.add(ymd) } else { selectedSet.delete(ymd) } }) } monthBox.appendChild(cell) } } function buildPadBlocksEnd(monthBox) { // 7×7 マスに揃えるための埋め草 while (monthBox.children.length < 49) { const cell = document.createElement('div') cell.className = 'cell' monthBox.appendChild(cell) } } /* ---------- 選択済み日付取得 ---------- */ function getSelectedDatesMap() { const res = {} months.forEach(({ month }) => { res[month + 1] = [] }) // 選択済み日付を振り分け selectedSet.forEach((ymd) => { const d = new Date(ymd) const displayMonth = d.getMonth() + 1 res[displayMonth].push(ymd) }) // 選択が無い月は全土日を自動追加 Object.keys(res).forEach((monthKey) => { if (res[monthKey].length === 0) { const monthInfo = months.find((m) => m.displayMonth == monthKey) if (monthInfo) { const year = monthInfo.year const month = monthInfo.month const lastDay = new Date(year, month + 1, 0).getDate() for (let day = 1; day <= lastDay; day++) { const dateObj = new Date(year, month, day) if (isWeekend(dateObj)) { res[monthKey].push(toYmd(dateObj)) } } } } }) return res } return { fragment, getSelectedDatesMap } } })()