欢迎来到关于如何创建简单的纯 Javascript 日历的教程。您是否希望在不使用任何服务器端脚本的情况下开发日历 Web 应用程序?无论是商业项目、学校项目还是只是出于好奇——您来对地方了。本指南将引导您了解如何创建纯 Javascript 日历,所有这些都可以通过本地存储实现。继续阅读!
ⓘ 我在本教程开始时包含了一个包含所有源代码的 zip 文件,因此您不必复制粘贴所有内容……或者如果您只是想直接进入。
速记
- 如果您想将日历设置为从星期一开始一周,请
cal.sMon = true
在calendar.js
.
如果您发现错误,请随时在下面发表评论。我也尝试回答简短的问题,但这是一个人与整个世界的对比……如果您急需答案,请查看我的网站列表以获取编程帮助。
示例代码下载
单击此处下载源代码,我已在 MIT 许可下发布它,因此请随意在其上构建或在您自己的项目中使用它。
JAVASCRIPT 日历演示
日历如何运作
好的,现在让我们更详细地了解日历的工作原理。不打算逐行解释,但这里有一个快速演练。
第 1 部分)日历 HTML
<div id="cal-wrap">
<!-- (A) PERIOD SELECTOR -->
<div id="cal-date">
<select id="cal-mth"></select>
<select id="cal-yr"></select>
</div>
<!-- (B) CALENDAR -->
<div id="cal-container"></div>
<!-- (C) EVENT FORM -->
<form id="cal-event">
<h1 id="evt-head"></h1>
<div id="evt-date"></div>
<textarea id="evt-details" required></textarea>
<input id="evt-close" type="button" value="Close"/>
<input id="evt-del" type="button" value="Delete"/>
<input id="evt-save" type="submit" value="Save"/>
</form>
</div>
HTML 应该足够简单,这里只有 3 个部分:
<div id="cal-date">
月份和年份选择器。<div id="cal-container">
我们将在其中显示所选月份和年份的日历。<form id="cal-event">
添加/编辑日历事件的表单。
第 2 部分)日历初始化
var cal = {
// (A) PROPERTIES
// (A1) COMMON CALENDAR
sMon : false, // Week start on Monday?
mName : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // Month Names
// (A2) CALENDAR DATA
data : null, // Events for the selected period
sDay : 0, sMth : 0, sYear : 0, // Current selected day, month, year
// (A3) COMMON HTML ELEMENTS
hMth : null, hYear : null, // month/year selector
hForm : null, hfHead : null, hfDate : null, hfTxt : null, hfDel : null, // event form
// (B) INIT CALENDAR
init : () => {
// (B1) GET + SET COMMON HTML ELEMENTS
cal.hMth = document.getElementById("cal-mth");
cal.hYear = document.getElementById("cal-yr");
cal.hForm = document.getElementById("cal-event");
cal.hfHead = document.getElementById("evt-head");
cal.hfDate = document.getElementById("evt-date");
cal.hfTxt = document.getElementById("evt-details");
cal.hfDel = document.getElementById("evt-del");
document.getElementById("evt-close").onclick = cal.close;
cal.hfDel.onclick = cal.del;
cal.hForm.onsubmit = cal.save;
// (B2) DATE NOW
let now = new Date(),
nowMth = now.getMonth(),
nowYear = parseInt(now.getFullYear());
// (B3) APPEND MONTHS SELECTOR
for (let i=0; i<12; i++) {
let opt = document.createElement("option");
opt.value = i;
opt.innerHTML = cal.mName[i];
if (i==nowMth) { opt.selected = true; }
cal.hMth.appendChild(opt);
}
cal.hMth.onchange = cal.list;
// (B4) APPEND YEARS SELECTOR
// Set to 10 years range. Change this as you like.
for (let i=nowYear-10; i<=nowYear+10; i++) {
let opt = document.createElement("option");
opt.value = i;
opt.innerHTML = i;
if (i==nowYear) { opt.selected = true; }
cal.hYear.appendChild(opt);
}
cal.hYear.onchange = cal.list;
// (B5) START - DRAW CALENDAR
cal.list();
};
window.addEventListener("load", cal.init);
cal.init()
是在页面加载时运行以初始化日历的第一件事。乍一看很复杂,但要冷静仔细看——它所做的只是设置HTML界面:
- 将月份添加到选择器。
- 向选择器添加年份。
- 附加添加/编辑事件表单单击并提交处理程序。
就这样。
第 3 部分)绘制日历
// (C) DRAW CALENDAR FOR SELECTED MONTH
list : () => {
// (C1) BASIC CALCULATIONS - DAYS IN MONTH, START + END DAY
// Note - Jan is 0 & Dec is 11
// Note - Sun is 0 & Sat is 6
cal.sMth = parseInt(cal.hMth.value); // selected month
cal.sYear = parseInt(cal.hYear.value); // selected year
let daysInMth = new Date(cal.sYear, cal.sMth+1, 0).getDate(), // number of days in selected month
startDay = new Date(cal.sYear, cal.sMth, 1).getDay(), // first day of the month
endDay = new Date(cal.sYear, cal.sMth, daysInMth).getDay(), // last day of the month
now = new Date(), // current date
nowMth = now.getMonth(), // current month
nowYear = parseInt(now.getFullYear()), // current year
nowDay = cal.sMth==nowMth && cal.sYear==nowYear ? now.getDate() : null ;
// (C2) LOAD DATA FROM LOCALSTORAGE
cal.data = localStorage.getItem("cal-" + cal.sMth + "-" + cal.sYear);
if (cal.data==null) {
localStorage.setItem("cal-" + cal.sMth + "-" + cal.sYear, "{}");
cal.data = {};
} else { cal.data = JSON.parse(cal.data); }
// (C3) DRAWING CALCULATIONS
// Blank squares before start of month
let squares = [];
if (cal.sMon && startDay != 1) {
let blanks = startDay==0 ? 7 : startDay ;
for (let i=1; i<blanks; i++) { squares.push("b"); }
}
if (!cal.sMon && startDay != 0) {
for (let i=0; i<startDay; i++) { squares.push("b"); }
}
// Days of the month
for (let i=1; i<=daysInMth; i++) { squares.push(i); }
// Blank squares after end of month
if (cal.sMon && endDay != 0) {
let blanks = endDay==6 ? 1 : 7-endDay;
for (let i=0; i<blanks; i++) { squares.push("b"); }
}
if (!cal.sMon && endDay != 6) {
let blanks = endDay==0 ? 6 : 6-endDay;
for (let i=0; i<blanks; i++) { squares.push("b"); }
}
// (C4) DRAW HTML CALENDAR
// Get container
let container = document.getElementById("cal-container"),
cTable = document.createElement("table");
cTable.id = "calendar";
container.innerHTML = "";
container.appendChild(cTable);
// First row - Day names
let cRow = document.createElement("tr"),
days = ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"];
if (cal.sMon) { days.push(days.shift()); }
for (let d of days) {
let cCell = document.createElement("td");
cCell.innerHTML = d;
cRow.appendChild(cCell);
}
cRow.classList.add("head");
cTable.appendChild(cRow);
// Days in Month
let total = squares.length;
cRow = document.createElement("tr");
cRow.classList.add("day");
for (let i=0; i<total; i++) {
let cCell = document.createElement("td");
if (squares[i]=="b") { cCell.classList.add("blank"); }
else {
if (nowDay==squares[i]) { cCell.classList.add("today"); }
cCell.innerHTML = `<div class="dd">${squares[i]}</div>`;
if (cal.data[squares[i]]) {
cCell.innerHTML += "<div class='evt'>" + cal.data[squares[i]] + "</div>";
}
cCell.onclick = () => { cal.show(cCell); };
}
cRow.appendChild(cCell);
if (i!=0 && (i+1)%7==0) {
cTable.appendChild(cRow);
cRow = document.createElement("tr");
cRow.classList.add("day");
}
}
// (C5) REMOVE ANY PREVIOUS ADD/EDIT EVENT DOCKET
cal.close();
}
cal.list()
绘制当前选择的月/年的日历,是本项目中最复杂的功能。长话短说:
- (C1 & C2)事件数据
localStorage
按月保存 (cal-MONTH-YEAR = JSON ENCODED OBJECT
)。 - (C1 & C2)检索所选月/年的事件数据,并将其放入
cal.data
. - (C3)首先进行所有计算
squares = []
。 - (C4)然后,循环
squares
绘制 HTML 日历。
第 4 部分)显示/编辑日历事件
// (D) SHOW EDIT EVENT DOCKET FOR SELECTED DAY
show : (el) => {
// (D1) FETCH EXISTING DATA
cal.sDay = el.getElementsByClassName("dd")[0].innerHTML;
let isEdit = cal.data[cal.sDay] !== undefined ;
// (D2) UPDATE EVENT FORM
cal.hfTxt.value = isEdit ? cal.data[cal.sDay] : "" ;
cal.hfHead.innerHTML = isEdit ? "EDIT EVENT" : "ADD EVENT" ;
cal.hfDate.innerHTML = `${cal.sDay} ${cal.mName[cal.sMth]} ${cal.sYear}`;
if (isEdit) { cal.hfDel.classList.remove("ninja"); }
else { cal.hfDel.classList.add("ninja"); }
cal.hForm.classList.remove("ninja");
},
// (E) CLOSE EVENT DOCKET
close : () => {
cal.hForm.classList.add("ninja");
}
当用户点击一个日期单元格时,cal.show()
将被调用。不言自明——我们从 获取事件cal.data
,然后显示添加/编辑事件表单。
第 5 部分)保存/删除事件数据
// (F) SAVE EVENT
save : () => {
cal.data[cal.sDay] = cal.hfTxt.value;
localStorage.setItem(`cal-${cal.sMth}-${cal.sYear}`, JSON.stringify(cal.data));
cal.list();
return false;
},
// (G) DELETE EVENT FOR SELECTED DATE
del : () => { if (confirm("Delete event?")) {
delete cal.data[cal.sDay];
localStorage.setItem(`cal-${cal.sMth}-${cal.sYear}`, JSON.stringify(cal.data));
cal.list();
}
最后,这些都是不言自明的函数——我们只是简单地更新localStorage
.
有用的信息和链接
最后,这里还有一些可能有用的附加功能。
每天有多个事件?
我们当然可以。我们来看看当前的save()
函数:
cal.data[cal.sDay] = cal.hfTxt.value;
我们现在几乎存储了事件数据data[DAY] = EVENT
。所以要允许同一天发生多个事件,大体思路是把当前结构变成多维数组data[DAY] = [EVENT, EVENT, EVENT]
。
cal.data[cal.sDay] = [];
cal.data[cal.sDay].push(document.getElementById("evt-details").value);
那么,大问题来了。我们必须重做所有其余的功能才能正确绘制多个事件并管理它们。
- 在 HTML 中添加更多文本字段。
save()
包括额外的文本字段。list()
绘制额外的数据。show()
也填充额外的文本字段。
你可以猜到,这将成倍增加复杂性,把它变成一个“不简单的教程”。这就是为什么我不回答“支持多个事件”和“跨越多天的事件”问题的原因——尽管请随意挑战自己,这个简单的日历是一个很好的起点。
限制
- 一天只允许一个事件。
- 事件不能跨越多天。
- Javascript 日历需要使用本地存储。如果它在浏览器上被禁用或不受支持,那么它将根本无法保存事件。
但是,当然,我已将其作为开源发布。因此,请随意调整它,但您认为合适。
完整汉化源码
calendar.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Simple Javascript Calendar</title>
<link href="calendar.css" rel="stylesheet">
<script async src="calendar.js"></script>
</head>
<body>
<div id="cal-wrap">
<!-- (A) PERIOD SELECTOR -->
<div id="cal-date">
<select id="cal-yr"></select>
<select id="cal-mth"></select>
</div>
<!-- (B) CALENDAR -->
<div id="cal-container"></div>
<!-- (C) EVENT FORM -->
<form id="cal-event">
<h1 id="evt-head"></h1>
<div id="evt-date"></div>
<textarea id="evt-details" required></textarea>
<input id="evt-close" type="button" value="Close"/>
<input id="evt-del" type="button" value="Delete"/>
<input id="evt-save" type="submit" value="Save"/>
</form>
</div>
</body>
</html>
calendar.css
/* (A) ENTIRE PAGE */
#cal-wrap * {
font-family: arial, sans-serif;
}
.ninja {
display: none !important;
}
/* (B) CONTAINER */
#cal-wrap {
max-width: 600px;
}
/* (C) PERIOD SELECTOR */
#cal-date {
display: flex;
justify-content: space-between;
}
#cal-mth, #cal-yr {
box-sizing: border-box;
padding: 10px 20px;
font-size: 1.2em;
border: 0;
}
/* (D) CALENDAR */
#calendar {
width: 100%;
border-collapse: collapse;
}
#calendar tr.head td {
font-weight: bold;
text-transform: uppercase;
color: #fff;
background: #f37070;
padding: 15px;
text-align: center;
}
#calendar tr.day td {
border: 1px solid #ddd;
width: 14.28%;
padding: 15px 5px;
vertical-align: top;
}
#calendar tr.day td:hover {
background: #fff9e4;
cursor: pointer;
}
#calendar tr td.blank {
background: #f5f5f5;
}
#calendar tr td.today {
background: #ffdede;
}
#calendar .dd {
font-size: 1.2em;
color: #999;
}
#calendar .evt {
margin-top: 5px;
font-size: 0.8em;
font-weight: bold;
overflow: hidden;
color: #ff5d5d;
}
/* (E) ADD/EDIT EVENT */
#cal-event {
padding: 15px;
margin-top: 20px;
background: #f5f5f5;
border: 1px solid #ddd;
}
#cal-event h1 {
color: #333;
padding: 0;
margin: 0;
}
#evt-date {
color: #555;
margin: 10px 0;
}
#cal-event textarea {
display: block;
box-sizing: border-box;
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
min-height: 100px;
}
#cal-event input[type=button], #cal-event input[type=submit] {
padding: 10px;
margin: 5px;
font-size: 1.2em;
border: 0;
background: #ea4c4c;
color: #fff;
}
calendar.js
var cal = {
// (A) PROPERTIES
// (A1) COMMON CALENDAR
sMon: true, // Week start on Monday?
mName: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], // Month Names
// (A2) CALENDAR DATA
data: null, // Events for the selected period
sDay: 0, sMth: 0, sYear: 0, // Current selected day, month, year
// (A3) COMMON HTML ELEMENTS
hMth: null, hYear: null, // month/year selector
hForm: null, hfHead: null, hfDate: null, hfTxt: null, hfDel: null, // event form
// (B) INIT CALENDAR
init: () => {
// (B1) GET + SET COMMON HTML ELEMENTS
cal.hMth = document.getElementById("cal-mth");
cal.hYear = document.getElementById("cal-yr");
cal.hForm = document.getElementById("cal-event");
cal.hfHead = document.getElementById("evt-head");
cal.hfDate = document.getElementById("evt-date");
cal.hfTxt = document.getElementById("evt-details");
cal.hfDel = document.getElementById("evt-del");
document.getElementById("evt-close").onclick = cal.close;
cal.hfDel.onclick = cal.del;
cal.hForm.onsubmit = cal.save;
// (B2) DATE NOW
let now = new Date(),
nowMth = now.getMonth(),
nowYear = parseInt(now.getFullYear());
// (B3) APPEND MONTHS SELECTOR
for (let i = 0; i < 12; i++) {
let opt = document.createElement("option");
opt.value = i;
opt.innerHTML = cal.mName[i];
if (i == nowMth) {
opt.selected = true;
}
cal.hMth.appendChild(opt);
}
cal.hMth.onchange = cal.list;
// (B4) APPEND YEARS SELECTOR
// Set to 10 years range. Change this as you like.
for (let i = nowYear - 10; i <= nowYear + 10; i++) {
let opt = document.createElement("option");
opt.value = i;
opt.innerHTML = i;
if (i == nowYear) {
opt.selected = true;
}
cal.hYear.appendChild(opt);
}
cal.hYear.onchange = cal.list;
// (B5) START - DRAW CALENDAR
cal.list();
},
// (C) DRAW CALENDAR FOR SELECTED MONTH
list: () => {
// (C1) BASIC CALCULATIONS - DAYS IN MONTH, START + END DAY
// Note - Jan is 0 & Dec is 11
// Note - Sun is 0 & Sat is 6
cal.sMth = parseInt(cal.hMth.value); // selected month
cal.sYear = parseInt(cal.hYear.value); // selected year
let daysInMth = new Date(cal.sYear, cal.sMth + 1, 0).getDate(), // number of days in selected month
startDay = new Date(cal.sYear, cal.sMth, 1).getDay(), // first day of the month
endDay = new Date(cal.sYear, cal.sMth, daysInMth).getDay(), // last day of the month
now = new Date(), // current date
nowMth = now.getMonth(), // current month
nowYear = parseInt(now.getFullYear()), // current year
nowDay = cal.sMth == nowMth && cal.sYear == nowYear ? now.getDate() : null;
// (C2) LOAD DATA FROM LOCALSTORAGE
cal.data = localStorage.getItem("cal-" + cal.sMth + "-" + cal.sYear);
if (cal.data == null) {
localStorage.setItem("cal-" + cal.sMth + "-" + cal.sYear, "{}");
cal.data = {};
} else {
cal.data = JSON.parse(cal.data);
}
// (C3) DRAWING CALCULATIONS
// Blank squares before start of month
let squares = [];
if (cal.sMon && startDay != 1) {
let blanks = startDay == 0 ? 7 : startDay;
for (let i = 1; i < blanks; i++) {
squares.push("b");
}
}
if (!cal.sMon && startDay != 0) {
for (let i = 0; i < startDay; i++) {
squares.push("b");
}
}
// Days of the month
for (let i = 1; i <= daysInMth; i++) {
squares.push(i);
}
// Blank squares after end of month
if (cal.sMon && endDay != 0) {
let blanks = endDay == 6 ? 1 : 7 - endDay;
for (let i = 0; i < blanks; i++) {
squares.push("b");
}
}
if (!cal.sMon && endDay != 6) {
let blanks = endDay == 0 ? 6 : 6 - endDay;
for (let i = 0; i < blanks; i++) {
squares.push("b");
}
}
// (C4) DRAW HTML CALENDAR
// Get container
let container = document.getElementById("cal-container"),
cTable = document.createElement("table");
cTable.id = "calendar";
container.innerHTML = "";
container.appendChild(cTable);
// First row - Day names
let cRow = document.createElement("tr"),
days = ["日", "一", "二", "三", "四", "五", "六"];
if (cal.sMon) {
days.push(days.shift());
}
for (let d of days) {
let cCell = document.createElement("td");
cCell.innerHTML = d;
cRow.appendChild(cCell);
}
cRow.classList.add("head");
cTable.appendChild(cRow);
// Days in Month
let total = squares.length;
cRow = document.createElement("tr");
cRow.classList.add("day");
for (let i = 0; i < total; i++) {
let cCell = document.createElement("td");
if (squares[i] == "b") {
cCell.classList.add("blank");
} else {
if (nowDay == squares[i]) {
cCell.classList.add("today");
}
cCell.innerHTML = `<div class="dd">${squares[i]}</div>`;
if (cal.data[squares[i]]) {
cCell.innerHTML += "<div class='evt'>" + cal.data[squares[i]] + "</div>";
}
cCell.onclick = () => {
cal.show(cCell);
};
}
cRow.appendChild(cCell);
if (i != 0 && (i + 1) % 7 == 0) {
cTable.appendChild(cRow);
cRow = document.createElement("tr");
cRow.classList.add("day");
}
}
// (C5) REMOVE ANY PREVIOUS ADD/EDIT EVENT DOCKET
cal.close();
},
// (D) SHOW EDIT EVENT DOCKET FOR SELECTED DAY
show: (el) => {
// (D1) FETCH EXISTING DATA
cal.sDay = el.getElementsByClassName("dd")[0].innerHTML;
let isEdit = cal.data[cal.sDay] !== undefined;
// (D2) UPDATE EVENT FORM
cal.hfTxt.value = isEdit ? cal.data[cal.sDay] : "";
cal.hfHead.innerHTML = isEdit ? "EDIT EVENT" : "ADD EVENT";
cal.hfDate.innerHTML = `${cal.sDay} ${cal.mName[cal.sMth]} ${cal.sYear}`;
if (isEdit) {
cal.hfDel.classList.remove("ninja");
} else {
cal.hfDel.classList.add("ninja");
}
cal.hForm.classList.remove("ninja");
},
// (E) CLOSE EVENT DOCKET
close: () => {
cal.hForm.classList.add("ninja");
},
// (F) SAVE EVENT
save: () => {
cal.data[cal.sDay] = cal.hfTxt.value;
localStorage.setItem(`cal-${cal.sMth}-${cal.sYear}`, JSON.stringify(cal.data));
cal.list();
return false;
},
// (G) DELETE EVENT FOR SELECTED DATE
del: () => {
if (confirm("Delete event?")) {
delete cal.data[cal.sDay];
localStorage.setItem(`cal-${cal.sMth}-${cal.sYear}`, JSON.stringify(cal.data));
cal.list();
}
}
};
window.addEventListener("load", cal.init);