note

Pikaday.js 祝日対応とMoment不使用フォーマット整形

実務でdatepickerを実装する必要があり、他への依存性が少なく軽量かつカスタマイズしやすいライブラリを探して、Pikaday を試してみることにしました。

選定の条件は下記の通り

  • 日本語対応可
  • 土日祝日対応
  • 日付フォーマット:yyyymmdd で出力可
  • cssデザインカスタマイズが容易

Pikaday単体では日付フォーマット変更オプションは用意されておらず、'Mon Jan 01 2018'という形式でしか出力できません。カスタマイズには自分でフォーマット処理を追加するか、公式推奨のMoment.jsを使用する必要があります。
今回、datepickerのためだけに依存ライブラリを増やしたくないので、公式に掲載されているMoment.js不使用の場合のフォーマット整形コードを参考に実装してみました。
また、祝日の指定にオプションのeventsを利用できますが、こちらも'Mon Jan 01 2018'のようにDateオブジェクト形式の文字列で配列を渡す必要があるため、シンプルに'2018/1/1'の型で配列を渡してから実行時にデータ型を整形するようにしました。

デザインについては、土日classが用意されていないので個別にスタイリングするためには、カレンダー部のtable構造を利用しfirst-child,last-childなどのセレクタで設定する必要があります。
また、オプションdisableWeekendsを利用して土日を選択不可にすることができますが、過去日と経過前の区別なくdisableする処理しか無いので、同じ選択不可でも過去と経過前のデザインを区別する場合はdisableWeekendsを利用せずに、過去は標準のdisableスタイルを適用し、経過前はcssのpointer-events:none;でボタンを無効化するなどの工夫が必要です。(オプションイベントonOpenやonDrawに処理を追加しても良いと思います)

html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title></title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.6.1/css/pikaday.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.6.1/pikaday.js"></script>
</head>
<body>
<label for="datepicker">Date:</label>
<br>
<input type="text" id="datepicker">
<script>
var pikadayHolidayArr = [ // holiday:祝日・振替休日・休業日
  '2018/1/1','2018/1/2','2018/1/3','2018/1/8',
  '2018/2/11','2018/2/12',
  '2018/3/21',
  '2018/4/29','2018/4/30',
  '2018/5/3','2018/5/4','2018/5/5',
  '2018/7/16',
  '2018/8/11',
  '2018/9/17','2018/9/23','2018/9/24',
  '2018/10/8',
  '2018/11/3','2018/11/23',
  '2018/12/23','2018/12/24','2018/12/31',
  '2019/1/1','2019/1/2','2019/1/3','2019/1/8',
  '2019/2/11','2019/2/12',
]
var pikadayConf = {
    toString: function(date, format) { // 出力フォーマット整形 形式「20180101」
        var day = ('0' + date.getDate()).slice(-2);
        var month = ('0' + (date.getMonth() + 1)).slice(-2);
        var year = date.getFullYear();
        var result = year+month+day;
        return result;
    },
    maxDate: function(){ // 表示期間リミット(Date オブジェクト)
        var newDate = new Date();
        var year = newDate.getFullYear() + 1; // 一年後
        var month = newDate.getMonth();
        var date = newDate.getDate();
        return new Date(year,month,date);
    },
    events: function() { // holiday データフォーマット変換 形式['Mon Jan 01 2018','Sun Dec 23 2018']
        var eventDays = [];
        var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
        var monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
        for (var i = 0, l = pikadayHolidayArr.length; i < l; i++) {
            var newDate = new Date(pikadayHolidayArr[i]);
            var day = dayNames[newDate.getDay()];
            var month = monthNames[newDate.getMonth()];
            var date = ('0' + newDate.getDate()).slice(-2);
            var year = newDate.getFullYear();
            eventDays.push(day + ' ' + month + ' ' + date + ' ' + year);
        }
        return eventDays;
    },
    i18n: {
        previousMonth : '前月',
        nextMonth     : '次月',
        months        : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月',],
        weekdays      : ['日曜日','月曜日','火曜日','水曜日','木曜日','金曜日','土曜日'],
        weekdaysShort : ['日','月','火','水','木','金','土']
    }
};
var datepicker = new Pikaday({
    field: document.getElementById('datepicker'),
    format: 'YYYYMMDD',
    toString: pikadayConf.toString,
    minDate: new Date(),
    maxDate: pikadayConf.maxDate(),
    events: pikadayConf.events(),
    disableWeekends: true,
    i18n: pikadayConf.i18n,
    yearSuffix: '年',
    showMonthAfterYear: true,
});
</script>
</body>
</html>

(1/28追記) 土日スタイルとpointer-events:none;での入力制限、範囲入力を追加

html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title></title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.6.1/css/pikaday.css">
<style>
.pika-lendar th:first-child,
.pika-lendar td:first-child .pika-button {
  color: #f00;
}
.pika-lendar th:last-child,
.pika-lendar td:last-child .pika-button {
  color: #00f;
}
.is-noevent-weekend .pika-row td:first-child button,
.is-noevent-weekend .pika-row td:last-child button {
  pointer-events: none;
}
.is-noevent-holiday .has-event button {
  pointer-events: none;
}
.is-startrange .pika-button {
  background-color: #000;
}
.is-endrange .pika-button {
  background-color: #000;
}
.is-inrange .pika-button {
  background-color: #eee;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.6.1/pikaday.js"></script>
</head>
<body>
<p>[single date]</p>
<label for="datepicker">Date:</label>
<br>
<input type="text" id="datepicker">
<br>
<br>
<p>[date-range]</p>
<div style="display: inline-block">
    <label for="start">Start:</label>
    <br>
    <input type="text" id="start">
</div>
<div style="display: inline-block">
    <label for="end">End:</label>
    <br>
    <input type="text" id="end">
</div>
<script>
var pikadayHolidayArr = [ // holiday:祝日・振替休日・休業日
  '2018/1/1','2018/1/2','2018/1/3','2018/1/8',
  '2018/2/11','2018/2/12',
  '2018/3/21',
  '2018/4/29','2018/4/30',
  '2018/5/3','2018/5/4','2018/5/5',
  '2018/7/16',
  '2018/8/11',
  '2018/9/17','2018/9/23','2018/9/24',
  '2018/10/8',
  '2018/11/3','2018/11/23',
  '2018/12/23','2018/12/24','2018/12/31',
  '2019/1/1','2019/1/2','2019/1/3','2019/1/8',
  '2019/2/11','2019/2/12',
]
var pikadayConf = {
    toString: function(date, format) { // 出力フォーマット整形 形式「20180101」
        var day = ('0' + date.getDate()).slice(-2);
        var month = ('0' + (date.getMonth() + 1)).slice(-2);
        var year = date.getFullYear();
        var result = year+month+day;
        return result;
    },
    maxDate: function(){ // 表示期間リミット(Date オブジェクト)
        var newDate = new Date();
        var year = newDate.getFullYear() + 1; // 一年後
        var month = newDate.getMonth();
        var date = newDate.getDate();
        return new Date(year,month,date);
    },
    events: function() { // holiday データフォーマット変換 形式['Mon Jan 01 2018','Sun Dec 23 2018']
        var eventDays = [];
        var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
        var monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
        for (var i = 0, l = pikadayHolidayArr.length; i < l; i++) {
            var newDate = new Date(pikadayHolidayArr[i]);
            var day = dayNames[newDate.getDay()];
            var month = monthNames[newDate.getMonth()];
            var date = ('0' + newDate.getDate()).slice(-2);
            var year = newDate.getFullYear();
            eventDays.push(day + ' ' + month + ' ' + date + ' ' + year);
        }
        return eventDays;
    },
    i18n: {
        previousMonth : '前月',
        nextMonth     : '次月',
        months        : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月',],
        weekdays      : ['日曜日','月曜日','火曜日','水曜日','木曜日','金曜日','土曜日'],
        weekdaysShort : ['日','月','火','水','木','金','土']
    },
    yearSuffix: '年',
};

// single date
var datepicker = new Pikaday({
    field: document.getElementById('datepicker'),
    format: 'YYYYMMDD',
    theme: 'is-noevent-weekend is-noevent-holiday',
    toString: pikadayConf.toString,
    minDate: new Date(),
    maxDate: pikadayConf.maxDate(),
    events: pikadayConf.events(),
//    disableWeekends: true,
    i18n: pikadayConf.i18n,
    yearSuffix: pikadayConf.yearSuffix,
    showMonthAfterYear: true,
});

// date-range
var startDate,
    endDate,
    updateStartDate = function() {
        startPicker.setStartRange(startDate);
        endPicker.setStartRange(startDate);
        endPicker.setMinDate(startDate);
    },
    updateEndDate = function() {
        startPicker.setEndRange(endDate);
        startPicker.setMaxDate(endDate);
        endPicker.setEndRange(endDate);
    },
    startPicker = new Pikaday({
        field: document.getElementById('start'),
        minDate: new Date(),
        maxDate: pikadayConf.maxDate(),
        onSelect: function() {
            startDate = this.getDate();
            updateStartDate();
        },
        format: 'YYYYMMDD',
        toString: pikadayConf.toString,
        events: pikadayConf.events(),
        i18n: pikadayConf.i18n,
        yearSuffix: pikadayConf.yearSuffix,
        showMonthAfterYear: true,
    }),
    endPicker = new Pikaday({
        field: document.getElementById('end'),
        minDate: new Date(),
        maxDate: pikadayConf.maxDate(),
        onSelect: function() {
            endDate = this.getDate();
            updateEndDate();
        },
        format: 'YYYYMMDD',
        toString: pikadayConf.toString,
        events: pikadayConf.events(),
        i18n: pikadayConf.i18n,
        yearSuffix: pikadayConf.yearSuffix,
        showMonthAfterYear: true,
    }),
    _startDate = startPicker.getDate(),
    _endDate = endPicker.getDate();
    if (_startDate) {
        startDate = _startDate;
        updateStartDate();
    }
    if (_endDate) {
        endDate = _endDate;
        updateEndDate();
    }
</script>
</body>
</html>

参考

以上、ありがとうございました。