效果


用法
            
            
              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) )]
]);