DatePicker 1 — 日付・期間選択
このコンポーネントについて
カレンダーポップアップとテキスト直接入力を組み合わせた日付選択コンポーネントです。
カレンダーから日付をクリックして選ぶだけでなく、入力欄に YYYY/MM/DD 形式で直接打ち込んでEnterキーで確定でき、存在しない日付はエラーメッセージでフィードバックします。
単一日付の選択(Pattern 1)と、開始日・終了日をそれぞれ独立したDatepickerで指定する期間選択(Pattern 2)の2パターンを収録しています。 予約フォームの日程指定・ダッシュボードの期間絞り込み・タスク管理の期限設定など、幅広い場面に応用できます。
- 単一日付選択(Pattern 1) — カレンダーまたはテキスト入力で1日を選択。「決定」ボタンで確定日を表示する
- 2ピッカー期間指定(Pattern 2) — 開始日・終了日を2つの独立したDatepickerで指定。片方だけ入力すれば「○○日 以降/以前」として扱える
実装のポイント・注意点
カレンダーポップアップの開閉は position: absolute で入力欄の真下に配置し、display: none / .is-open { display: block } で切り替えます。ポップアップ外のクリックで閉じる処理は document.addEventListener('click', ...) で検知し、e.stopPropagation() は使わずに closest('.dp-wrap') で内外を判定するのがポイントです(他のUIとの干渉を防ぐため)。
テキスト入力のバリデーションは「形式チェック」と「実在チェック」の2段階で行います。new Date(2026, 1, 30) のように JavaScript の Date は自動的に翌月にずれるため、new Date(y, m-1, d).getDate() === d で元の日と一致するかを確認することで存在しない日付(2月30日など)を弾けます。
Pattern 2 の整合制御は「後から確定した方を優先してもう一方をクリアする」方式です。開始日確定時に終了日と比較し、開始 > 終了になる場合は終了日をクリアします。
デモ
Pattern 1 — 単一日付選択
Pattern 2 — 期間指定(2つのDatepicker)
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DatePicker サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- Pattern 1: 単一日付選択 -->
<div class="dp-pattern">
<p class="dp-pattern-label">Pattern 1 — 単一日付選択</p>
<div class="dp-wrap" id="dp1">
<div class="dp-field">
<div class="dp-input-row">
<input type="text" class="dp-input" id="dp1-input"
placeholder="YYYY/MM/DD" autocomplete="off" aria-label="日付を入力">
<button class="dp-icon-btn" id="dp1-toggle" type="button" aria-label="カレンダーを開く">📅</button>
</div>
<p class="dp-error" id="dp1-error" aria-live="polite"></p>
</div>
<div class="dp-calendar" id="dp1-cal" aria-hidden="true">
<div class="dp-cal-header">
<button class="dp-nav-btn" id="dp1-prev" type="button">‹</button>
<span class="dp-cal-title" id="dp1-title"></span>
<button class="dp-nav-btn" id="dp1-next" type="button">›</button>
</div>
<div class="dp-weekdays">
<span class="dp-wd dp-wd--sun">日</span>
<span class="dp-wd">月</span><span class="dp-wd">火</span>
<span class="dp-wd">水</span><span class="dp-wd">木</span>
<span class="dp-wd">金</span>
<span class="dp-wd dp-wd--sat">土</span>
</div>
<div class="dp-cal-grid" id="dp1-grid"></div>
</div>
</div>
<button class="dp-confirm-btn" id="dp1-confirm" type="button">決定</button>
<p class="dp-result" id="dp1-result"></p>
</div>
<!-- Pattern 2: 期間指定(2つのDatepicker) -->
<div class="dp-pattern">
<p class="dp-pattern-label">Pattern 2 — 期間指定(2つのDatepicker)</p>
<div class="dp-range-row">
<div class="dp-range-item">
<label class="dp-label" for="dp2-start-input">開始日</label>
<div class="dp-wrap">
<div class="dp-field">
<div class="dp-input-row">
<input type="text" class="dp-input" id="dp2-start-input" placeholder="YYYY/MM/DD" autocomplete="off">
<button class="dp-icon-btn" id="dp2-start-toggle" type="button" aria-label="カレンダーを開く">📅</button>
</div>
<p class="dp-error" id="dp2-start-error" aria-live="polite"></p>
</div>
<div class="dp-calendar" id="dp2-start-cal" aria-hidden="true">
<div class="dp-cal-header">
<button class="dp-nav-btn" id="dp2-start-prev" type="button">‹</button>
<span class="dp-cal-title" id="dp2-start-title"></span>
<button class="dp-nav-btn" id="dp2-start-next" type="button">›</button>
</div>
<div class="dp-weekdays">
<span class="dp-wd dp-wd--sun">日</span>
<span class="dp-wd">月</span><span class="dp-wd">火</span>
<span class="dp-wd">水</span><span class="dp-wd">木</span>
<span class="dp-wd">金</span>
<span class="dp-wd dp-wd--sat">土</span>
</div>
<div class="dp-cal-grid" id="dp2-start-grid"></div>
</div>
</div>
</div>
<span class="dp-range-sep">〜</span>
<div class="dp-range-item">
<label class="dp-label" for="dp2-end-input">終了日</label>
<div class="dp-wrap">
<div class="dp-field">
<div class="dp-input-row">
<input type="text" class="dp-input" id="dp2-end-input" placeholder="YYYY/MM/DD" autocomplete="off">
<button class="dp-icon-btn" id="dp2-end-toggle" type="button" aria-label="カレンダーを開く">📅</button>
</div>
<p class="dp-error" id="dp2-end-error" aria-live="polite"></p>
</div>
<div class="dp-calendar" id="dp2-end-cal" aria-hidden="true">
<div class="dp-cal-header">
<button class="dp-nav-btn" id="dp2-end-prev" type="button">‹</button>
<span class="dp-cal-title" id="dp2-end-title"></span>
<button class="dp-nav-btn" id="dp2-end-next" type="button">›</button>
</div>
<div class="dp-weekdays">
<span class="dp-wd dp-wd--sun">日</span>
<span class="dp-wd">月</span><span class="dp-wd">火</span>
<span class="dp-wd">水</span><span class="dp-wd">木</span>
<span class="dp-wd">金</span>
<span class="dp-wd dp-wd--sat">土</span>
</div>
<div class="dp-cal-grid" id="dp2-end-grid"></div>
</div>
</div>
</div>
</div>
<button class="dp-confirm-btn" id="dp2-confirm" type="button">決定</button>
<p class="dp-result" id="dp2-result"></p>
</div>
<script src="./script.js"></script>
</body>
</html>
/* === DatePicker パターン集 ===
--dp-accent の値を変えるだけで配色を一括変更できます */
:root {
--dp-accent: #2B7FE8; /* メインカラー(選択状態・ボタン等) */
--dp-danger: #E53E3E; /* エラー・日曜日の色 */
--dp-sat: #2B7FE8; /* 土曜日の色 */
--dp-text: #1A2332; /* メインテキスト色 */
--dp-muted: #9AA5B4; /* サブテキスト色 */
--dp-border: #D0D7E0; /* ボーダー色 */
--dp-bg: #F4F6F9; /* カード背景色 */
}
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
max-width: 680px;
margin: 0 auto;
background: #fff;
color: var(--dp-text);
}
/* パターンカード */
.dp-pattern {
margin-bottom: 20px;
padding: 20px;
background: var(--dp-bg);
border-radius: 8px;
}
.dp-pattern:last-child { margin-bottom: 0; }
.dp-pattern-label {
margin: 0 0 14px;
font-size: 11px;
font-weight: 700;
color: var(--dp-accent);
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* ===== Datepicker 共通 ===== */
.dp-wrap { position: relative; display: inline-block; }
.dp-field { display: inline-flex; flex-direction: column; }
.dp-input-row { display: flex; align-items: center; }
.dp-input {
width: 140px;
padding: 7px 10px;
font-size: 14px;
border: 1px solid var(--dp-border);
border-radius: 4px 0 0 4px;
outline: none;
font-family: sans-serif;
color: var(--dp-text);
transition: border-color 0.15s;
}
.dp-input:focus { border-color: var(--dp-accent); }
.dp-input.is-error { border-color: var(--dp-danger); }
.dp-icon-btn {
display: flex; align-items: center; justify-content: center;
width: 34px; height: 34px; padding: 0;
border: 1px solid var(--dp-border);
border-left: none;
border-radius: 0 4px 4px 0;
background: #fff;
color: #5A6A7A;
cursor: pointer;
font-size: 16px;
transition: background 0.15s;
flex-shrink: 0;
}
.dp-icon-btn:hover { background: var(--dp-bg); }
.dp-error {
min-height: 16px; margin: 4px 0 0;
font-size: 12px; color: var(--dp-danger); line-height: 1.4;
}
/* カレンダーポップアップ */
.dp-calendar {
display: none;
position: absolute; top: calc(100% + 4px); left: 0;
z-index: 200;
background: #fff;
border: 1px solid var(--dp-border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
padding: 12px; width: 270px;
}
.dp-calendar.is-open { display: block; }
.dp-cal-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
}
.dp-cal-title { font-size: 14px; font-weight: 700; }
.dp-nav-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; padding: 0;
font-size: 18px; line-height: 1;
background: none; border: 1px solid var(--dp-border);
border-radius: 4px; cursor: pointer; color: #5A6A7A;
transition: background 0.15s; font-family: sans-serif;
}
.dp-nav-btn:hover { background: var(--dp-bg); }
.dp-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: 4px; }
.dp-wd { text-align: center; font-size: 11px; font-weight: 600; color: var(--dp-muted); padding: 3px 0; }
.dp-wd--sun { color: var(--dp-danger); }
.dp-wd--sat { color: var(--dp-sat); }
.dp-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dp-cell {
display: flex; align-items: center; justify-content: center;
height: 32px; font-size: 13px; border-radius: 4px;
cursor: pointer; color: var(--dp-text);
transition: background 0.1s; user-select: none;
}
.dp-cell:hover { background: #EFF4FD; }
.dp-cell--other-month { color: #C5CAD4; cursor: default; }
.dp-cell--other-month:hover { background: transparent; }
.dp-cell--sun { color: var(--dp-danger); }
.dp-cell--sat { color: var(--dp-sat); }
.dp-cell--today:not(.dp-cell--selected) {
font-weight: 700; color: var(--dp-accent);
}
/* 単一選択の選択セル */
.dp-cell--selected {
background: var(--dp-accent) !important;
color: #fff !important; border-radius: 50%; font-weight: 600;
}
/* ===== Pattern 2: 横並び ===== */
.dp-range-row { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.dp-range-item { display: flex; flex-direction: column; }
.dp-label { display: block; font-size: 12px; font-weight: 600; color: #5A6A7A; margin-bottom: 4px; }
.dp-range-sep { align-self: flex-end; padding-bottom: 10px; font-size: 18px; color: var(--dp-muted); }
/* ===== 決定ボタン・結果 ===== */
.dp-confirm-btn {
display: inline-block; margin-top: 14px; padding: 8px 24px;
background: var(--dp-accent); color: #fff; border: none;
border-radius: 6px; font-size: 14px; font-weight: 600;
cursor: pointer; font-family: sans-serif; transition: opacity 0.15s;
}
.dp-confirm-btn:hover { opacity: 0.85; }
.dp-result { margin: 8px 0 0; font-size: 14px; min-height: 20px; }
.dp-result.has-value {
padding: 10px 14px; background: #F0F7FF;
border: 1px solid var(--dp-accent); border-radius: 6px;
color: #1A5EA8; font-weight: 600;
}
.dp-result.is-error { color: var(--dp-danger); }
@media (max-width: 540px) {
body { padding: 16px; }
.dp-range-row { flex-direction: column; gap: 12px; }
.dp-range-sep { display: none; }
}
// ===== 共通ユーティリティ =====
// "YYYY/MM/DD" 文字列を { year, month, day } に分解する
function parseDate(str) {
var parts = str.split('/');
if (parts.length !== 3) return null;
var y = parseInt(parts[0], 10), m = parseInt(parts[1], 10), d = parseInt(parts[2], 10);
if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
return { year: y, month: m, day: d };
}
// 年・月・日が実在する日付かチェックする(2/30 などを弾く)
function isValidDate(y, m, d) {
if (y < 1900 || y > 2100) return false;
var dt = new Date(y, m - 1, d);
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
}
// Date オブジェクトを "YYYY年M月D日" 形式に変換する
function toJapaneseStr(dt) {
return dt.getFullYear() + '年' + (dt.getMonth() + 1) + '月' + dt.getDate() + '日';
}
function pad2(n) { return String(n).padStart(2, '0'); }
// ===== Datepicker ファクトリー関数(Pattern 1 / 2 で共用) =====
function createDatepicker(ids) {
var inputEl = document.getElementById(ids.input);
var toggleEl = document.getElementById(ids.toggle);
var calEl = document.getElementById(ids.calendar);
var titleEl = document.getElementById(ids.title);
var gridEl = document.getElementById(ids.grid);
var errorEl = document.getElementById(ids.error);
var prevEl = document.getElementById(ids.prev);
var nextEl = document.getElementById(ids.next);
var today = new Date();
var viewYear = today.getFullYear(), viewMonth = today.getMonth() + 1;
var selectedDate = null, onChangeCb = null;
function renderGrid() {
titleEl.textContent = viewYear + '年' + viewMonth + '月';
var firstDay = new Date(viewYear, viewMonth - 1, 1).getDay();
var lastDate = new Date(viewYear, viewMonth, 0).getDate();
var prevLast = new Date(viewYear, viewMonth - 1, 0).getDate();
var total = Math.ceil((firstDay + lastDate) / 7) * 7;
var html = '';
for (var i = 0; i < total; i++) {
var day, yr, mo, isOther = false;
if (i < firstDay) {
mo = viewMonth === 1 ? 12 : viewMonth - 1;
yr = viewMonth === 1 ? viewYear - 1 : viewYear;
day = prevLast - firstDay + i + 1; isOther = true;
} else if (i >= firstDay + lastDate) {
mo = viewMonth === 12 ? 1 : viewMonth + 1;
yr = viewMonth === 12 ? viewYear + 1 : viewYear;
day = i - firstDay - lastDate + 1; isOther = true;
} else { day = i - firstDay + 1; yr = viewYear; mo = viewMonth; }
var dateStr = yr + '/' + pad2(mo) + '/' + pad2(day);
var dow = i % 7;
var cls = ['dp-cell'];
if (isOther) {
cls.push('dp-cell--other-month');
} else {
if (dow === 0) cls.push('dp-cell--sun');
if (dow === 6) cls.push('dp-cell--sat');
if (yr === today.getFullYear() && mo === today.getMonth() + 1 && day === today.getDate()) cls.push('dp-cell--today');
if (selectedDate && yr === selectedDate.getFullYear() &&
mo === selectedDate.getMonth() + 1 && day === selectedDate.getDate()) cls.push('dp-cell--selected');
}
html += '<span class="' + cls.join(' ') + '" data-date="' + dateStr + '"' + (isOther ? ' data-other="true"' : '') + '>' + day + '</span>';
}
gridEl.innerHTML = html;
}
function open() {
// 他のカレンダーを閉じてから開く
document.querySelectorAll('.dp-calendar.is-open').forEach(function (c) { if (c !== calEl) c.classList.remove('is-open'); });
calEl.classList.add('is-open'); renderGrid();
}
function close() { calEl.classList.remove('is-open'); }
function selectDate(dateStr) {
var p = dateStr.split('/');
selectedDate = new Date(parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2]));
inputEl.value = dateStr;
inputEl.classList.remove('is-error'); errorEl.textContent = '';
viewYear = selectedDate.getFullYear(); viewMonth = selectedDate.getMonth() + 1;
if (calEl.classList.contains('is-open')) renderGrid();
if (onChangeCb) onChangeCb(selectedDate);
}
function confirmInput() {
var str = inputEl.value.trim();
if (!str) { selectedDate = null; inputEl.classList.remove('is-error'); errorEl.textContent = ''; if (onChangeCb) onChangeCb(null); return; }
var parsed = parseDate(str);
if (!parsed || !isValidDate(parsed.year, parsed.month, parsed.day)) {
inputEl.classList.add('is-error'); errorEl.textContent = '有効な日付を入力してください(例: 2026/5/20)'; return;
}
selectDate(parsed.year + '/' + pad2(parsed.month) + '/' + pad2(parsed.day));
}
toggleEl.addEventListener('click', function (e) { e.stopPropagation(); calEl.classList.contains('is-open') ? close() : open(); });
inputEl.addEventListener('click', function (e) { e.stopPropagation(); open(); });
inputEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); confirmInput(); close(); } });
gridEl.addEventListener('click', function (e) {
var cell = e.target.closest('.dp-cell');
if (!cell || cell.dataset.other) return;
selectDate(cell.dataset.date); close();
});
prevEl.addEventListener('click', function () { viewMonth--; if (viewMonth < 1) { viewMonth = 12; viewYear--; } renderGrid(); });
nextEl.addEventListener('click', function () { viewMonth++; if (viewMonth > 12) { viewMonth = 1; viewYear++; } renderGrid(); });
renderGrid();
return {
open: open, close: close,
getValue: function () { return selectedDate; },
clear: function () {
selectedDate = null; inputEl.value = ''; inputEl.classList.remove('is-error'); errorEl.textContent = '';
viewYear = today.getFullYear(); viewMonth = today.getMonth() + 1;
if (calEl.classList.contains('is-open')) renderGrid();
},
setOnChange: function (cb) { onChangeCb = cb; }
};
}
// ===== Pattern 1: 単一日付選択 =====
var dp1 = createDatepicker({ input: 'dp1-input', toggle: 'dp1-toggle', calendar: 'dp1-cal', title: 'dp1-title', grid: 'dp1-grid', error: 'dp1-error', prev: 'dp1-prev', next: 'dp1-next' });
document.getElementById('dp1-confirm').addEventListener('click', function () {
var result = document.getElementById('dp1-result');
var val = dp1.getValue();
if (!val) { result.textContent = '※ 日付を選択してください'; result.className = 'dp-result is-error'; return; }
result.textContent = '選択日: ' + toJapaneseStr(val);
result.className = 'dp-result has-value';
});
// ===== Pattern 2: 期間指定(2つのDatepicker) =====
var dp2Start = createDatepicker({ input: 'dp2-start-input', toggle: 'dp2-start-toggle', calendar: 'dp2-start-cal', title: 'dp2-start-title', grid: 'dp2-start-grid', error: 'dp2-start-error', prev: 'dp2-start-prev', next: 'dp2-start-next' });
var dp2End = createDatepicker({ input: 'dp2-end-input', toggle: 'dp2-end-toggle', calendar: 'dp2-end-cal', title: 'dp2-end-title', grid: 'dp2-end-grid', error: 'dp2-end-error', prev: 'dp2-end-prev', next: 'dp2-end-next' });
// 整合制御: 開始 > 終了になる場合、もう一方をクリアする
dp2Start.setOnChange(function (s) { var e = dp2End.getValue(); if (s && e && s.getTime() > e.getTime()) dp2End.clear(); });
dp2End.setOnChange(function (e) { var s = dp2Start.getValue(); if (s && e && e.getTime() < s.getTime()) dp2Start.clear(); });
document.getElementById('dp2-confirm').addEventListener('click', function () {
var result = document.getElementById('dp2-result');
var s = dp2Start.getValue(), e = dp2End.getValue();
if (!s && !e) { result.textContent = '※ 日付を入力してください'; result.className = 'dp-result is-error'; return; }
if (s && e) { result.textContent = '期間: ' + toJapaneseStr(s) + ' 〜 ' + toJapaneseStr(e); }
else if (s) { result.textContent = toJapaneseStr(s) + ' 以降'; }
else { result.textContent = toJapaneseStr(e) + ' 以前'; }
result.className = 'dp-result has-value';
});
// ===== ドキュメントクリックでポップアップを閉じる =====
document.addEventListener('click', function () {
document.querySelectorAll('.dp-calendar.is-open').forEach(function (c) { c.classList.remove('is-open'); });
});
AI用プロンプト
各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や要件の追記をして使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
Pattern 1 — 単一日付選択
# DatePicker(単一日付選択)作成依頼
## 概要
テキスト直接入力とカレンダーポップアップを組み合わせた日付選択コンポーネントを実装してください。
## 要件
- 入力欄クリックでカレンダーポップアップを表示する
- カレンダーから日付を選択すると入力欄に YYYY/MM/DD 形式で反映し、カレンダーを閉じる
- 入力欄に YYYY/MM/DD を直接入力してEnterキーで確定できる
- 確定時に日付の存在チェックを行い、不正な日付(例: 2026/02/30)は赤枠+エラーメッセージを表示する
- 入力欄外クリックでカレンダーを閉じる
- カレンダーに前月・次月ボタンを設置し月を移動できる
- 今日の日付をカレンダー上でハイライト表示する
- 土曜は青、日曜は赤で色分けする
- 「決定」ボタンを設置し、押下で選択日を「選択日: YYYY年MM月DD日」形式で表示する
- 未選択で決定ボタンを押した場合はエラーメッセージを表示する
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
- カレンダーのグリッドは月初の曜日に合わせて前月末日でパディングし、7列×5〜6行で生成する
- 前月・次月のはみ出し日はグレーアウト表示し、クリックを無効にする
- テキスト入力のバリデーション: parseDate で形式チェック → new Date() で実在チェックの2段階
- ポップアップ外クリック検知は document.addEventListener('click') で行い、e.stopPropagation() は使わずに .closest('.dp-wrap') で内外を判定する
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。
Pattern 2 — 期間指定(2つのDatepicker)
# DatePicker(期間指定・2ピッカー型)作成依頼
## 概要
開始日と終了日をそれぞれ独立したDatepickerで指定する期間選択コンポーネントを実装してください。
## 要件
- 開始日・終了日それぞれにテキスト入力+カレンダーポップアップのDatepickerを配置する
- 各Datepickerの動作: テキスト直接入力・Enter確定・バリデーション・カレンダーポップアップ(単一選択型と同じ)
- 整合制御: 開始日 > 終了日になる矛盾が発生した場合、後から確定した方を優先し、もう一方をクリアする
- カレンダーは同時に1つだけ開く(片方を開いたら他方を閉じる)
- 「決定」ボタン押下で結果を表示する:
- 両方入力済み → 「期間: YYYY年MM月DD日 〜 YYYY年MM月DD日」
- 開始日のみ → 「YYYY年MM月DD日 以降」
- 終了日のみ → 「YYYY年MM月DD日 以前」
- 両方未入力 → エラーメッセージ
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(スマホでは開始・終了を縦並びにする)
## 動作詳細
- Datepickerのコアロジック(グリッド生成・入力パース・バリデーション)はファクトリー関数として共通化し2インスタンス生成する
- 整合チェックは各Datepickerの日付確定タイミングでコールバックを呼び出して実行する
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。