效果


用法
js
import * as BaseCalendar from './base-calendar/index.js';
function main() {
BaseCalendar.StyleSheet().then(
sheet => document.adoptedStyleSheets.push(sheet)
);
document.body.append(
BaseCalendar.Component({ date: new Date() }).identity,
);
}
main();
规格
七列。
七行,一行表头 一二三四五六日;六行日期。
组成
显示上个月、本月和下个月的天数。
需要的日数据
- 上个月天数最后的几天。
- 这个月的所有天数。
- 下个月的所有天数。
到底补上个月最后几天,取决于本月 1 日的 周几 - 1
。
比如 2023 年 12 月,1 日在周五,所以补充上个月的最后四天。
最后拼接好三段数组,截取出前 42 个。
概览
-
BaseCalendar.Component()
组件函数,会返回必要的元素节点和函数。
-
identity
:DocumentFragment
本体。
-
state
必要的状态和 HTML 元素。
js
// base-calendar/index.js
import { ElemsHTML, Frag } from '@/util/dom.js';
const tableSize = { row: 6, col: 7 };
export function StyleSheet() { ... }
export function Component(props) {
function render() { ... }
function init() { ... }
const identity = Frag(
`<table class="base-calendar">
<thead>
<tr>
${ ElemsHTML(R.map(s => `<th>${ s }</th>`), ['一', '二', '三', '四', '五', '六', '日']) }
</tr>
</thead>
<tbody><!-- --></tbody>
</table>`
)
.firstElementChild;
const state = {
date: new Date(),
get tds() {
return identity.querySelectorAll('tbody td');
},
};
init();
render();
return {
identity,
render,
}
}
样式
js
// base-calendar/index.js
export function StyleSheet() {
const sheet = new CSSStyleSheet();
return sheet.replace(`
.base-calendar {
text-align: center;
border-collapse: collapse;
&, th, td { border: 1px solid currentColor; }
td {
width: 2em;
height: 2em;
}
}
`)
}
初始化
用 props
初始化 state
,给 <tbody>
里添加空的 <td>
。
js
// base-calendar/index.js
import { ElemsHTML, Frag } from '@/util/dom.js';
export function Component(props) {
function render() { ... }
function init() {
props.date && (state.date = props.date);
identity.querySelector('tbody').innerHTML = ElemsHTML(
R.map(() => (
`<tr>${
ElemsHTML(R.map(() => `<td></td>`), R.range(0, tableSize.col))
}</tr>`)
),
R.range(0, tableSize.row)
);
}
const identity = Frag(
/* ... */
)
.firstElementChild;
const state = {
/* ... */
};
init();
render();
return {
identity,
render,
};
}
初步成果

渲染
用日期数字填充所有 <td>
。
本月1日在第 1 行 周x 列。
本月日期之前和之后,分别填上个月和下个月的。
-
DateInfoDic()
上本下三个月的相关信息。
-
DatesDic
上本下三个月的日期数组。
js
import { DateInfoDic, DatesDic } from './use-date-info.js';
export function Component(props) {
function render() {
const dateInfoDic = DateInfoDic(state.date);
const datesDic = DatesDic(dateInfoDic);
R.compose(
R.addIndex(R.forEach)((n, i) => (state.tds[i].innerText = n)),
R.take(state.tds.length),
R.unnest
)([
/* 上个月天数往回推 本月1日的周x 个,再 +1 */
R.slice(
datesDic.prevMonth.length - dateInfoDic.currMonth.dayOfWeek + 1,
Number.POSITIVE_INFINITY,
datesDic.prevMonth
),
datesDic.currMonth,
datesDic.nextMonth
]))
}
function init() {
/* ... */
}
const identity = Frag(
/* ... */
)
.firstElementChild;
const state = {
/* ... */
};
init();
render();
return {
identity,
render,
};
}
日期信息
js
import { DayNumOfMonth } from '@/util/date.js';
/**
* 年、月下标、日期、周日期
* @param {Date} date
* @returns {import('./type').DateInfo}
*/
function DateInfo(date) {
return {
year: date.getFullYear(),
monthIndex: date.getMonth(),
dayOfMonth: date.getDate(),
/** getDay() 其他都是,1-6,周日得把 0 变 7`, */
dayOfWeek: date.getDay() || 7,
};
}
/** x年x月1日 `Date` */
const FstDateOfMonth = ({ year, monthIndex }) => new Date(year, monthIndex);
/** `DateInfo` 转成相邻月份1日 */
const toAdjacentMonth = (monthIndexCond, TF, RF) => (
R.ifElse( R.compose( monthIndexCond, R.prop('monthIndex') ), TF, RF )
);
/**
* x年x月1日 `DateInfo`
* @param {Function} f
* @param {Date} date
* @returns {import('./type').DateInfo}
*/
const FstDateInfoOfMonth = (f, date) => R.compose( DateInfo, FstDateOfMonth, f, DateInfo )(date);
/**
* 本月和相邻月份1日 `Date`
* @param {Date} date
* @returns {import('./type').DateInfoDic}
*/
export const DateInfoDic = date => ({
get prevMonth() {
return FstDateInfoOfMonth(
toAdjacentMonth(
R.gt(R.__, 0),
R.evolve({ monthIndex: R.dec }),
R.evolve({ year: R.dec, monthIndex: R.always(11) })
),
date
)
},
get currMonth() { return FstDateInfoOfMonth(R.identity, date) },
get nextMonth() {
return FstDateInfoOfMonth(
toAdjacentMonth(
R.lt(R.__, 11),
R.evolve({ monthIndex: R.inc }),
R.evolve({ year: R.inc, monthIndex: R.always(0) })
),
date
)
},
});
/**
* @type {function(import('./type').DateInfoDic): Record<import('./type').NeedMonth, number[]>}
*/
export const DatesDic = R.map(R.compose( R.range(1), R.inc, DayNumOfMonth ));
工具函数
util/dom
-
ElemsHTML()
string[]
->string
-
Frag()
创建
<template>
,赋值innerHTML
,返回content
。
js
// util/dom.js
/** @type {function( transducer: Function, any[]): string} */
export const ElemsHTML = R.transduce(R.__, R.concat, '', R.__);
/** @type {(html: any) => DocumentFragment} */
export function Frag(html) {
const tmplElem = document.createElement('template');
tmplElem.innerHTML = html;
return tmplElem.content;
};
util/date
-
isLeapYear()
判断年份是否为闰年
-
DayNumOfMonth()
算出月份的天数
js
// util/date.js
/** @type {(year: number) => boolean} */
export const isLeapYear = R.anyPass([
R.compose( R.equals(0), R.modulo(R.__, 400) ),
R.allPass([
R.compose( R.equals(0), R.modulo(R.__, 4) ),
R.compose( R.not, R.equals(0), R.modulo(R.__, 100) )
])
]);
/** @type {(month: number) => number} */
export const DayNumOfMonth = R.cond([
[R.compose( R.includes(R.__, [1,3,5,7,8,10,12]), R.inc, R.prop('monthIndex') ), R.always(31)],
[R.compose( R.includes(R.__, [4,6,9,11]), R.inc, R.prop('monthIndex') ), R.always(30)],
[R.T, R.ifElse( R.compose( isLeapYear, R.prop('year') ), R.always(29), R.always(28) )]
]);