Last active
September 28, 2023 07:54
-
-
Save CarsonSlovoka/f1a3ac4c701b7bc25d92ddd5389eda1e to your computer and use it in GitHub Desktop.
example: simple calendar (js)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @typedef {Object} CalendarOptions | |
* @property {function(Calendar): void} execute | |
*/ | |
/** | |
* @typedef {Object} StyleOptions | |
* @property {(style: Object)=>{}} execute | |
*/ | |
/** | |
* @example | |
* // Calendar.initCSSStyle({execute: (style) => {style.backgroundColor="yellow"}}, {execute: (style) => {style.backgroundColor="yellow"}}) | |
* Calendar.initCSSStyle() | |
* const calendar = new Calendar(document.querySelector('div#calendar-container'), new Date()) | |
* calendar.onToggleDate = ()=>{ | |
* document.getElementById('dates').value = calendar.selectedDates.join(', ') | |
* } | |
**/ | |
export class Calendar { | |
// 用來表示每一個Calendar所產生的物件,其id | |
static #SerialNumber = 0 | |
/** | |
* @param {StyleOptions} options | |
**/ | |
static initCSSStyle(...options) { | |
// 檢查有沒有定義 | |
for (const stylesheet of document.styleSheets) { | |
for (const rule of stylesheet.cssRules) { | |
// rule.cssText // 會包含定義的所有內容 | |
if (rule.selectorText.includes("table.calendar")) { // rule.selectorText: 就是css前面的名稱,例如 "rect:hover + .tooltip" | |
console.info("您已經嘗試自定義table.calendar的樣式,所以不再加入預設的style") | |
return | |
} | |
} | |
} | |
const style = { | |
width: "40px", | |
height: "40px", | |
border: "1px solid #ddd", | |
backgroundColor: "white", | |
selected: { | |
color: "#fff", | |
backgroundColor: "#00f", | |
}, | |
today: { | |
color: "#ecffec", | |
backgroundColor: "#000", | |
}, | |
} | |
for (const opt of options) { | |
opt.execute(style) | |
} | |
const css = ` | |
table.calendar { | |
display: none; | |
position: absolute; | |
background-color: ${style.backgroundColor}; | |
border: ${style.border}; | |
} | |
table.calendar th, table.calendar td { | |
width: ${style.width}; | |
height: ${style.height}; | |
text-align: center; | |
vertical-align: middle; | |
border: ${style.border}; | |
} | |
table.calendar td.selected { | |
background-color: ${style.selected.backgroundColor}!important; | |
color: ${style.selected.color}!important; | |
} | |
table.calendar td.today { | |
background-color: ${style.today.backgroundColor}; | |
color: ${style.today.color} | |
} | |
` | |
// 加入自定義css | |
const styleSheet = document.createElement("style") | |
styleSheet.innerText = css | |
document.head.appendChild(styleSheet) | |
} | |
/** | |
* @param {HTMLElement} container | |
* @param {Date} currentDate 日曆所顯示的月份,以此數值為主 | |
* @param {CalendarOptions} options | |
**/ | |
constructor(container, currentDate = new Date(), ...options) { | |
container.innerHTML = '' | |
this.container = container | |
this.currentDate = currentDate | |
// 紀錄當前選中的項目 | |
/** @param {string[]} selectedDates | |
* @example ['2023/09/28'] | |
* */ | |
this.selectedDates = [] | |
this.table = document.createElement("table") | |
this.table.classList.add("calendar") | |
this.id = Calendar.#SerialNumber | |
Calendar.#SerialNumber++ | |
// 在document之中新增一個calendarMap屬性,使的HTML的部分能直接使用document來獲得到目前的物件 | |
if (document.calendarMap === undefined) { | |
document.calendarMap = {} | |
} | |
document.calendarMap[`${this.id}`] = this | |
this.table.innerHTML = ` | |
<thead> | |
<tr> | |
<!-- colspan 總共要有7,表示一周7天 --> | |
<th colspan="1"><button onclick="document.calendarMap[${this.id}].goToPreviousMonth(event)">◀</button></th> | |
<th colspan="5" id="monthLabel"></th> | |
<th colspan="1"><button onclick="document.calendarMap[${this.id}].goToNextMonth(event)">▶</button></th> | |
</tr> | |
<tr class="day-header"> | |
<th>Sun</th> | |
<th>Mon</th> | |
<th>Tue</th> | |
<th>Wed</th> | |
<th>Thu</th> | |
<th>Fri</th> | |
<th>Sat</th> | |
</tr> | |
</thead> | |
<tbody></tbody>` | |
// 給自定義選項使用 | |
this.optionsVar = {} | |
// 啟用使用者的自定義選項 | |
for (const optFunc of options) { | |
optFunc.execute(this) | |
} | |
this.container.append(this.table) | |
} | |
/** | |
* @description 顯示或者隱藏日曆 | |
**/ | |
toggle() { | |
if (this.table.style.display === 'none' || this.table.style.display === '') { | |
this.table.style.display = 'block' | |
this.#render() | |
} else { | |
this.table.style.display = 'none' | |
} | |
} | |
/** | |
* @param {PointerEvent} e | |
* @description 會更新currentDate | |
**/ | |
goToPreviousMonth(e) { | |
e.preventDefault() // 避免可能會送出表單 | |
this.currentDate.setDate(1) // 避免這種case而不正確的問題new Date(2023, 10, 0) // 10/31 => month-1 => 10/1 | |
this.currentDate.setMonth(this.currentDate.getMonth() - 1); | |
this.#render() | |
} | |
// 會更新currentDate | |
goToNextMonth(e) { | |
e.preventDefault() | |
this.currentDate.setDate(1) | |
this.currentDate.setMonth(this.currentDate.getMonth() + 1); | |
this.#render() | |
} | |
#render() { | |
const today = new Date(Date.now()) | |
const tBody = this.table.tBodies[0] | |
tBody.innerHTML = "" | |
const startDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1) | |
const endDate = new Date(this.currentDate.getFullYear(), startDate.getMonth() + 1, 0) // date 0會是目前月份的前一個月的最後一天 | |
let row = tBody.insertRow() | |
let cellDay = -1 | |
let cell | |
for (const d = startDate; d <= endDate; d.setDate(d.getDate() + 1)) { | |
if (row.cells.length === 7) { | |
row = tBody.insertRow() | |
cellDay = -1 | |
} | |
cell = row.insertCell() | |
++cellDay | |
for (; d.getDay() !== cellDay || cellDay >= 7 ; ++cellDay) { | |
cell = row.insertCell() | |
} | |
if (cellDay === 7) { | |
continue | |
} | |
cell.innerText = `${d.getDate()}` | |
// 創建一個額外的屬性保留選中的日期詳細資訊 | |
cell.dataset.date = d.toLocaleDateString('zh-TW', {year: 'numeric', month: '2-digit', day: '2-digit'}) // 2023/09/28 // d.toISOString().slice(0, 10) // ISOString: 2022-02-21T05:59:02.737Z. ISO會轉成UTC+0的時間,兩者會不一致 | |
cell.onclick = (e) => this.#toggleDate(e) | |
// 替toady加上class來特別識別 | |
// if (d - today === 0) { // 時間不同,所以不會相等 | |
if (cell.dataset.date === today.toLocaleDateString('zh-TW', { | |
year: 'numeric', | |
month: '2-digit', | |
day: '2-digit' | |
})) { | |
cell.classList.add('today') | |
} | |
// 初始化狀態 | |
if (this.selectedDates.includes(cell.dataset.date)) { | |
cell.classList.add('selected') | |
} | |
// 年月標題更新 | |
document.getElementById('monthLabel').textContent = this.currentDate.toLocaleDateString('default', { | |
month: 'long', | |
year: 'numeric' | |
}) | |
} | |
} | |
/** | |
* @param {MouseEvent} e | |
**/ | |
#toggleDate(e) { | |
const cell = e.target | |
const date = cell.dataset.date | |
if (cell.classList.contains("selected")) { | |
cell.classList.remove("selected") | |
this.selectedDates.splice(this.selectedDates.indexOf(date), 1) // 移除這個日期,其他的照舊 | |
} else { | |
cell.classList.add("selected") | |
this.selectedDates.push(date) | |
} | |
// 讓使用者可以自己自訂選中日期時的額外動作 | |
this.onToggleDate(this) | |
} | |
/** | |
* @description 可以自定義選中日期時的額外動作 | |
**/ | |
onToggleDate() { | |
} | |
} | |
/** | |
* @param {Number} ms millisecond 閒置多少毫秒之後才會自動關閉 | |
* @return {CalendarOptions} | |
**/ | |
export function withAutoClose(ms) { | |
return { | |
execute: (calendar) => { | |
calendar.table.addEventListener("mouseenter", () => { | |
if (calendar.optionsVar.hideTimer) { | |
clearTimeout(calendar.optionsVar.hideTimer) | |
calendar.optionsVar.hideTimer = null | |
} | |
}) | |
calendar.table.addEventListener("mouseleave", () => { | |
calendar.optionsVar.hideTimer = setTimeout(() => { | |
calendar.table.style.display = 'none' | |
}, | |
ms) | |
}) | |
} | |
} | |
} | |
/** | |
* @param {Array<String>} headers "['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] ['日', '一', '二', '三', '四', '五', '六']" | |
* @return {CalendarOptions} | |
**/ | |
export function withHeader(headers) { | |
return { | |
execute: (calendar) => { | |
if (headers.length !== 7) { | |
console.error("headers.length !== 7") | |
return | |
} | |
[...calendar.table.querySelectorAll('tr.day-header th')].forEach((td, idx)=>{ | |
td.innerText = headers[idx] | |
}) | |
} | |
} | |
} |
Author
CarsonSlovoka
commented
Sep 28, 2023
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment