JS: 簡易 Date Picker
如何使用 JavaScript 實作一個 date picker。參考教學: Custom Date Picker in JavaScript & CSS。
這邊做一下筆記如何完成以及哪邊該注意的。
起始結構
- index.html
- reset.css
- style.css
- app.js
- simple-date-picker
- simple-date-picker.css
- simple-date-picker.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Date Picker</title>
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="./simple-date-picker/simple-date-picker.css">
</head>
<body>
<div class="container">
<div id="date-picker"></div>
</div>
<script src="./simple-date-picker/simple-date-picker.js"></script>
<script src="app.js"></script>
</body>
</html>
style.css
.container {
width: 100vw;
height: 100vh;
background: rgb(249,242,206);
background: linear-gradient(156deg, rgba(249,242,206,1) 2%, rgba(184,230,246,1) 96%);
display: flex;
justify-content: center;
align-items: center;
}
#date-picker {
width: 350px;
}
simple-date-picker.css
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
.simple-date-picker {
font-family: 'Roboto', sans-serif;
position: relative;
width: 350px;
height: 70px;
background-color: mintcream;
margin: 10px auto;
box-shadow: 0px 2px 6px rgba(0,0,0,.2);
cursor: pointer;
user-select: none;
}
.simple-date-picker:hover {
background-color: #e9f9ff;
}
.simple-date-picker .selected-date {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #313131;
font-size: 30px;
}
.simple-date-picker .dates {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: #fff;
}
// active 展開
.simple-date-picker .dates.active {
display: block;
}
simple-date-picker.js
const datePicker = ({root}) => {
root.innerHTML = `
<div class="simple-date-picker">
<div class="selected-date">08-25-2020</div>
<div class="dates">
<div class="month">
<div class="btns prev-month"><<</div>
<div class="month-label">八月</div>
<div class="btns next-month">>></div>
</div>
<div class="days"></div>
</div>
</div>
`;
const datepickerElement = root.querySelector('.simple-date-picker');
const selectedDateElement = root.querySelector('.simple-date-picker .selected-date');
const datesElement = root.querySelector('.simple-date-picker .dates');
datepickerElement.addEventListener('click', toggleDatepicker);
function toggleDatepicker(e){
console.log('展開 datepicker')
}
}
這邊的先用一個函式打包,方便以後可以在其他地方使用,也可以一次使用多個,不會互相打架。
app.js
datePicker({
root: document.querySelector('#date-picker'),
});
在 app.js 裡呼叫,生成一個 date picker,順便指定要載入的元素 #date-picker。
展開/關閉日期選擇器
toggle 開關
現在日期選擇器可以展開,但是如果滑鼠點擊『月份選擇』區塊時,它會關閉。這時候需要檢查滑鼠所點擊的位置有沒有在 .dates
區塊。
在 simple-date-picker.js 裡面新增一個 helper function。
...(略)
datepickerElement.addEventListener('click', toggleDatepicker);
function toggleDatepicker(e){
console.log(e.path);
if (!checkEventPathForClass(e.path, 'dates')) {
datesElement.classList.toggle('active');
}
}
function checkEventPathForClass (path, selector) {
for (let i = 0; i < path.length; i++) {
if (path[i].classList && path[i].classList.contains(selector)) {
console.log('path[i].classList', path[i].classList)
console.log('contains selector? ', path[i].classList.contains(selector))
return true;
}
}
return false;
}
當 date picker 展開時,滑鼠點擊下方『月份選擇』區塊時,e.path 是有包含 selector dates
,所以會 return true
。
if (!checkEventPathForClass(e.path, 'dates')) {
datesElement.classList.toggle('active');
}
而我們要 checkEventPathForClass 非 true 時,才關閉日期選擇器。
點擊以外元素&關閉
如果要點擊 #date-picker (root) 以外的地方就關閉日期選擇棄,也可以用類似方法達到。
document.addEventListener('click', function(e){
if (!root.contains(e.target)){
datesElement.classList.remove('active');
}
});
判斷 root
element (date picker 所有元素)有沒有包含滑鼠點擊的元素(e.target
)。頭兩次的 e.target 都沒有包含所以選擇器不會關閉,到了第三個才會 remove class active
。
月份 & 日期選擇器
CSS Style
/* 月份選擇 */
.simple-date-picker .dates .month {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 3px solid #eee;
padding: 5px;
}
/* 月份選擇 左右按鈕 */
.simple-date-picker .dates .month .btns {
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
color: #313131;
font-size: 20px;
border-radius: 50%;
}
.simple-date-picker .dates .month .btns:hover {
background-color: whitesmoke;
color: #999;
}
.simple-date-picker .dates .month .btns:active {
background-color: azure;
color: #bbb;
}
在月份選擇區塊加入樣式。
/* 日期選擇器 */
.simple-date-picker .dates .days {
height: 240px;
display: grid;
grid-template-columns: repeat(7, 1fr);
border-radius: 5px;
}
.simple-date-picker .dates .days .day {
display: flex;
justify-content: center;
align-items: center;
color: #313131;
font-size: 15px;
font-weight: 300;
border-radius: 50%;
border: 1px solid #eee;
background-color: #eee;
margin: 5px;
}
.simple-date-picker .dates .days .day:hover {
font-weight: 700;
background-color: #fff;
border: 1px solid #eee;
}
.simple-date-picker .dates .days .day:active,
.simple-date-picker .dates .days .day.selected {
font-weight: 700;
background-color: lightsalmon;
border: 1px solid lightsalmon;
color: #fff;
}
在日期區塊加入樣式。
const datePicker = ({root}) => {
root.innerHTML = `
<div class="simple-date-picker">
<div class="selected-date">08-25-2020</div>
<div class="dates">
<div class="month">
<div class="btns prev-month"><<</div>
<div class="month-label">八月</div>
<div class="btns next-month">>></div>
</div>
<div class="days">
<!-- 這部分先加入日期,測試一下樣式 -->
<div class="day">1</div>
<div class="day">2</div>
<div class="day">3</div>
<div class="day">4</div>
<div class="day">5</div>
<div class="day">6</div>
<div class="day">7</div>
<div class="day">8</div>
<div class="day">9</div>
<div class="day">10</div>
<div class="day">11</div>
<div class="day">12</div>
<div class="day">13</div>
<div class="day">14</div>
<div class="day">15</div>
<div class="day">16</div>
<div class="day">17</div>
<div class="day">18</div>
<div class="day">19</div>
<div class="day">20</div>
<div class="day">21</div>
<div class="day">22</div>
<div class="day">23</div>
<div class="day">24</div>
<div class="day">25</div>
<div class="day">26</div>
<div class="day">27</div>
<div class="day">28</div>
<div class="day">29</div>
<div class="day">30</div>
<div class="day">31</div>
</div>
</div>
</div>
`;
.... 略
Date Picker 預設日期
const datePicker = ({root}) => {
root.innerHTML = `
<div class="simple-date-picker">
<!-- 填入的日期先清空,要讓 JS 自動填入當下的日期 -->
<div class="selected-date"></div>
<div class="dates">
<div class="month">
<div class="btns prev-month"><<</div>
<div class="month-label"></div>
<div class="btns next-month">>></div>
</div>
<div class="days"></div>
</div>
</div>
`;
... 略
selectedDateElement.textContent = formatDate(date);
function formatDate(d) {
let day = d.getDate();
let month = d.getMonth();
let year = d.getFullYear();
if (day < 10) {
day = `0${day}`;
}
if (month < 10){
month = `0${month+1}`
}
return `${year}年 ${month}月 ${day}日`;
}
展開時顯示月份年份
const monthElement = root.querySelector('.simple-date-picker .dates .month .month-label');
const prevBtn = root.querySelector('.simple-date-picker .dates .month .prev-month');
const nextBtn = root.querySelector('.simple-date-picker .dates .month .next-month');
const daysElement = root.querySelector('.simple-date-picker .dates .days');
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// 當下日期
let date = new Date();
let day = date.getDate();
let month = date.getMonth();
let year = date.getFullYear();
// 選擇的日期
let selectedDate = date;
let selectedDay = day;
let selectedMonth = month;
let selectedYear = year;
monthElement.textContent = `${ month+1 }月 ${year}年`; // 展開時顯示的月份&年份
月份往前/後移動&更新畫面
prevBtn.addEventListener('click', goPrevMonth); // 往前一個月事件監聽
nextBtn.addEventListener('click', goNextMonth); // 往後一個月事件監聽
// 當月份往前到 -1 時,要換成 11 (十二月),年也要跟著減1
function goPrevMonth() {
month--;
if (month < 0) {
month = 11;
year--;
}
monthElement.textContent = `${ month+1 }月 ${year}年`; // 展開時顯示的月份&年份
}
// 當月份走到12時 (即十三月),要換成 0 (一月),年也要跟著加1
function goNextMonth() {
month++;
if (month > 11) {
month = 0;
year++;
}
monthElement.textContent = `${ month+1 }月 ${year}年`; // 展開時顯示的月份&年份
}
加入對應日期
populateDates(month, year);
function goPrevMonth() {
... 略
populateDates(month, year);
}
function goNextMonth() {
... 略
populateDates(month, year);
}
function populateDates(mth, yr) {
daysElement.innerHTML = '';
let amountDays = getTotalDays(mth, yr); // 取得月份共幾天
for (let i = 0; i < amountDays; i++) {
const dayElement = document.createElement('div');
dayElement.classList.add('day');
dayElement.textContent = i + 1;
daysElement.appendChild(dayElement);
}
}
function getTotalDays(month, year) {
return new Date(year, month+1, 0).getDate();
}
當要加入對應的日期數量需要先知道被選擇的月份有幾天,所以下面的 getTotalDays() 函式會回傳月份日數,再用這個去跑回圈。
日期事件監聽
當進入 populateDates() 回圈時,需要幫 dayElement 元素個別加入事件監聽,這樣才知道到底選擇了哪一天。
function populateDates(mth, yr) {
daysElement.innerHTML = '';
let amountDays = getTotalDays(mth, yr); // 取得月份共幾天
for (let i = 0; i < amountDays; i++) {
const dayElement = document.createElement('div');
dayElement.classList.add('day');
dayElement.textContent = i + 1;
++ // 遇到當下日期/被選擇的日期,加上 css style
++ if (selectedDay == (i + 1) && selectedYear == year && selectedMonth == month) {
++ dayElement.classList.add('selected');
++ }
++ dayElement.addEventListener('click', function() {
++ selectedDate = new Date(year + '-' + (month+1) + '-' + (i+1));
++ selectedDay = (i + 1);
++ selectedMonth = month;
++ selectedYear = year;
++ selectedDateElement.textContent = formatDate(selectedDate);
++ selectedDateElement.dataset.value = dataSetFormat(selectedDate); // 在元素上加入 dataset,之後要取得日期比較容易。
++ populateDates(selectedMonth, selectedYear); // 當選擇了某一天,日期區塊需要重新更新。
++ });
daysElement.appendChild(dayElement);
}
}
function dataSetFormat(d) {
const year = d.getFullYear();
const month = d.getMonth()+1;
const day = d.getDate();
return [year, month, day].join("-");
}
之後要取得選擇的日期可以直接從 data-value 取得。
加入週
/* WEEK */
.simple-date-picker .dates .weekday {
display: grid;
grid-template-columns: repeat(7, 1fr);
border-radius: 5px;
padding: 8px;
}
.simple-date-picker .dates .weekday .week {
display: flex;
justify-content: center;
align-items: center;
color: #313131;
font-size: 13px;
font-weight: 600;
margin: 15px 0 0 0;
}
.simple-date-picker .dates .days .space {
display: flex;
justify-content: center;
align-items: center;
margin: 5px;
height: 36px;
}
function populateDates(mth, yr) {
daysElement.innerHTML = '';
let amountDays = getTotalDays(mth, yr); // 取得月份共幾天
++ let firstDay = getStartedWeekDay(mth, yr);
++ let spacer = ``;
++ for (let i = 0; i < firstDay; i++) {
++ spacer += `<div class="space"></div>`;
++ }
++ daysElement.innerHTML = spacer;
... 略
}
function getStartedWeekDay(month, year) {
return new Date(year, month, 1).getDay();
}
使用 getStartedWeekDay() 取得月份起始日是週幾。 getDay()
會回傳本地時間星期中的日子(0-6,從星期日開始)。之後在 daysElement 加入幾個空格,把月份的 1 號撐開至對應的星期日子。
Reference:
Custom Date Picker in JavaScript & CSS
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。