Skip to content

Instantly share code, notes, and snippets.

@CarsonSlovoka
Last active September 28, 2023 07:54
Show Gist options
  • Save CarsonSlovoka/f1a3ac4c701b7bc25d92ddd5389eda1e to your computer and use it in GitHub Desktop.
Save CarsonSlovoka/f1a3ac4c701b7bc25d92ddd5389eda1e to your computer and use it in GitHub Desktop.
example: simple calendar (js)
/**
* @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)">&#9664;</button></th>
<th colspan="5" id="monthLabel"></th>
<th colspan="1"><button onclick="document.calendarMap[${this.id}].goToNextMonth(event)">&#9654;</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]
})
}
}
}
@CarsonSlovoka
Copy link
Author

CarsonSlovoka commented Sep 28, 2023

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment