使用 Javascript 编写日历插件

2025-06-08

使用 Javascript 编写日历插件

介绍

说到 JavaScript,我们经常会想到库和框架。如今,JavaScript 的使用方法五花八门,数不胜数。然而,我们常常忘记,即使没有框架或库,我们仍然可以使用经典的 JavaScript。在本文中,我们将只使用原生 JavaScript 构建一个插件。这个插件非常简单,可以在 HTML 页面中添加日历。

设置

我们需要三个文件:一个 HTML 文件、一个 CSS 文件和一个 JavaScript 文件。我们先从 JavaScript 文件开始,因为这是我们工作最多的地方。

插件骨架

(function (root, factory) {
  root.myCalendar = factory(root);
})(this, (root) => {
  let privateVar = "No, No, No...";
  let init = () => {
    console.log("Init the calendar");
  };
  return {
    init,
  };
});

我们要做的第一件事就是让我们的插件在我们的环境中可用。我们使用 IIFE(立即调用函数表达式)来实现这一点。如你所见,我们将第一个函数括在括号中,使其成为一个可以立即调用的表达式。

IIFE 对于封装代码非常有用。我的插件代码将无法从插件外部访问。我们稍后再讨论这一点。

让我们稍微分解一下上面的代码:

在我们的函数主体中我们执行以下操作:

root.myCalendar = factory(root);

root是什么?这是我们 IIFE 的第一个参数,也就是 this。所以,在浏览器中, this 就是window对象。我们将window.myCalendar设置为factory(root)。我们的 IIFE 的第二个参数factory是一个函数。这实际上就是我们的插件内容。

这种方法的妙处在于,window.myCalendar只会包含我的函数返回的内容。因此,我可以调用window.myCalendar.init(),但window.myCalendar.privateVar将是未定义的,因为它不是由我们的 IIFE 返回的。

导入我们的index.html

我们已经有一个插件了!它功能不多,但很管用。让我们创建一个 HTML 文件来测试一下。

<html>
  <head>
    <script src="simple-calendar.js"></script>
    <script>
      window.onload = function () {
        myCalendar.init();
        console.log(myCalendar.privateVar);
      };
    </script>
  </head>
  <body></body>
</html>

我们加载 JavaScript 文件。我将其命名为simple-calendar.js,但您可以随意命名。然后,在窗口加载完成后,在onload事件监听器中,调用myCalendar.init()并在 console.log 中记录myCalendar.privateVar变量。

注意: window.myCalendarmyCalendar在这里是相同的;)

以下是我在控制台中看到的内容:

控制台私有变量

太棒了!init函数打印出了我们预期的内容,而 privateVar 确实是undefined,因为它不是从我们的 IIFE 返回的,所以我们的插件不知道你在说什么!

CSS

我们先把这个问题解决掉,因为这不是本文的重点。创建一个 CSS 文件,并在其中添加以下样式:

#calendar {
  background: #fff;
  border-radius: 4px;
  color: #222629;
  overflow: hidden;
  margin-top: 20px;
  max-width: 400px;
}

#calendar.hidden {
  display: none;
}

button {
  border: none;
}

#calendar .header {
  background: #ddd;
  height: 40px;
  line-height: 40px;
  text-align: center;
}

#calendar .header + div {
  border: 1px solid black;
}

#calendar .month {
  display: inline-block;
  font-weight: bold;
}

#calendar button {
  background: none;
  color: inherit;
  cursor: pointer;
  font-size: 23px;
  font-weight: bold;
  height: 100%;
  padding: 0 15px;
}

#calendar button:first-child {
  float: left;
}

#calendar button:last-child {
  float: right;
}

#calendar .cell {
  background: #fff;
  color: #5d5d5d;
  box-sizing: border-box;
  display: inline-block;
  padding: 10px 0;
  text-align: center;
  width: calc(100% / 7);
  cursor: pointer;
}

#calendar .cell:hover {
  color: white;
  background-color: blue;
}

#calendar .day {
  font-size: 0.8rem;
  padding: 8px 0;
}

#calendar .cell.today {
  background-color: blue;
  color: white;
}

#calendar .day {
  color: black;
}

别忘了把它导入到我们的 HTML 文件中。在<head>页面的末尾,添加以下代码:

<link rel="stylesheet" href="calendar.css" />

当然,用您的文件名称替换calendar.css 。

添加功能

好的,它非常可爱,但是我的插件在这里仍然没有做任何事情......让我们开始吧。

月份、日期和今天

我首先需要获取月份列表、日期列表和今天的日期。我希望我的日历默认显示今天的日期。因此,在我们的插件中,在私有变量上方,添加以下内容:

// Beginning of the file cut for brevity
    let monthList = new Array(
      "january",
      "february",
      "march",
      "april",
      "may",
      "june",
      "july",
      "august",
      "september",
      "october",
      "november",
      "december"
    );
    let dayList = new Array(
      "sunday",
      "monday",
      "tuesday",
      "wednesday",
      "thursday",
      "friday",
      "saturday"
    );
    let today = new Date();
    today.setHours(0, 0, 0, 0);
    let privateVar = "No, No, No...";

  let init = () => {
    console.log("Init the calendar");
  };
  return {
    init,
  };
});

好,一切设置完毕。现在,我们可以开始修改 DOM 来实现日历了。显然,这一步需要在init函数中完成。我们希望在初始化插件时就能显示日历。

我们需要做几件事:

  • 创建一个包含当前月份和年份名称的标题。此标题还应包含“下一页”和“上一页”按钮,用于在月份之间导航。

  • 标题下方是日期列表,从星期日到星期一。

  • 最后,我们将得到当前月份的天数。

标题

// Our variables are up there
let init = () => {
  let element = document.getElementById("calendar");

  let currentMonth = new Date(today.getFullYear(), today.getMonth(), 1);

  // Creating the div for our calendar's header
  let header = document.createElement("div");
  header.classList.add("header");
  element.appendChild(header);

  // Our "previous" button
  let previousButton = document.createElement("button");
  previousButton.setAttribute("data-action", "-1");
  previousButton.textContent = "\u003c";
  header.appendChild(previousButton);

  // Creating the div that will contain the actual month/year
  let monthDiv = document.createElement("div");
  monthDiv.classList.add("month");
  header.appendChild(monthDiv);

  // Our "next" button
  let nextButton = document.createElement("button");
  nextButton.setAttribute("data-action", "1");
  nextButton.textContent = "\u003e";
  header.appendChild(nextButton);
};

这里我们只用 JavaScript 添加了一些元素。我们没有使用任何花哨的 JavaScript API,只是使用了createElementappendChildsetAttribute等经典的 JavaScript API 。我们创建了标题栏的 div 元素,用于包含当前月份的名称。我们还创建了上一个和下一个按钮。

请注意这一行:

let element = document.getElementById("calendar");

这个元素将包含我们的日历。我们把它放在一个 id 为calendar 的元素中。这是我做的选择,但我们稍后会让它可自定义。但这意味着我们需要在 HTML 中添加一个具有正确 id 的元素:

<!-- The <head> tag is up there-->
<body>
  <div id="calendar"></div>
</body>

HTML 代码就是这样。果然,我们可以在页面中看到 header 了。

标题日历

我们继续前进吧!

添加日期列表和月份单元格

现在,让我们添加包含当前月份日期的单元格。我们需要注意的是:月初的“空”日期。我们的一周从星期日开始,但如果月份从星期三开始,我们就需要填充一些空单元格。

为了清楚起见,我将把这个逻辑放在它自己的方法中。

// This is inside the init function, right before the end of the function

 // Creating the div that will contain the days of our calendar
    let content = document.createElement("div");
    element.appendChild(content);

    // Load current month
    // monthDiv is the element in the header that will contain the month's name
    // content is the element that will contain our days' cells
    // We created those variables earlier in the function
    loadMonth(currentMonth, content, monthDiv);
    } // <- this is the end of the init function

  let loadMonth = (date, content, monthDiv) => {
    // Empty the calendar
    content.textContent = "";

    // Adding the month/year displayed
    monthDiv.textContent =
      monthList[date.getMonth()].toUpperCase() + " " + date.getFullYear();

    // Creating the cells containing the days of the week
    // I've created a separate method for this
    createDaysNamesCells(content);

    // Creating empty cells if necessary
    createEmptyCellsIfNecessary(content, date);


    // Number of days in the current month
    let monthLength = new Date(
      date.getFullYear(),
      date.getMonth() + 1,
      0
    ).getDate();

    // Creating the cells containing current's month's days
    for (let i = 1; i <= monthLength; i++) {
      let cell = document.createElement("span");
      cell.classList.add("cell");
      cell.textContent = `${i}`;
      content.appendChild(cell);

      // Cell's timestamp
      let timestamp = new Date(
        date.getFullYear(),
        date.getMonth(),
        i
      ).getTime();
      cell.addEventListener("click", () => {
        console.log(timestamp);
        console.log(new Date(timestamp))
      });

      // Add a special class for today
      if (timestamp === today.getTime()) {
        cell.classList.add("today");
      }
    }
  }

  let createDaysNamesCells = (content) => {
    for (let i = 0; i < dayList.length; i++) {
      let cell = document.createElement("span");
      cell.classList.add("cell");
      cell.classList.add("day");
      cell.textContent = dayList[i].substring(0, 3).toUpperCase();
      content.appendChild(cell);
    }
  };

  let createEmptyCellsIfNecessary = content => {
    for (let i = 0; i < date.getDay(); i++) {
      let cell = document.createElement("span");
      cell.classList.add("cell");
      cell.classList.add("empty");
      content.appendChild(cell);
    }
  }

  // The rest of the plugin down here, cut for brevity

这里发生了很多事!

  • 我们首先调用loadMonth。此函数负责在标题中显示当前月份和当前年份的名称。

  • 然后我们调用createDaysNamesCells来显示周日到周六的列表。

  • 如果需要,我们调用createEmptyCellsIfNecessary来显示空单元格。我们将date变量赋给该函数,该变量代表当前月份的第一天。通过对该变量调用getDay()函数,我们可以获取日期的索引。由于日期从星期日开始,就像日历中的星期一样,所以我们可以执行一个简单的循环来渲染所需的空单元格数量。

  • 最后,我们获取该月的天数,并在每个单元格中显示正确的日期。我们在每个单元格上添加了一个事件监听器,用于在控制台中打印所选日期的时间戳和日期。我们还为当前日期添加了一个类,该类将使用 CSS 进行样式设置。

这是目前的结果!

完整日历

日历已正确呈现,当我们单击日期时,我们会在控制台中看到时间戳和我们单击的单元格的日期。

添加交互性

我们需要添加三件事:

  • 当我点击某个日期时,它就变成选定的日期。
  • 当我单击上一个按钮时,我们会转到上一个月。
  • 当我点击下一步按钮时,我们进入下个月。

对于第一项,我们需要将“today”类添加到正确的单元格。我们还需要从之前选定的单元格中删除“today”类。 “today”是我选择的类名,但您可以随意命名。您只需适当地更新代码即可。导航到我们将时间戳和日期打印到控制台的位置,并将代码更改为:

cell.addEventListener("click", () => {
  console.log(timestamp);
  console.log(new Date(timestamp));
  document.querySelector(".cell.today")?.classList.remove("today");
  cell.classList.add("today");
});

这将正确设置您选择的单元格的样式。

最后,我们将添加下一个/上一个月的功能:

//Inside the init function

// Next/previous button functionality
element.querySelectorAll("button").forEach((element) => {
  element.addEventListener("click", () => {
    currentMonth.setMonth(
      currentMonth.getMonth() * 1 +
        parseInt(element.getAttribute("data-action")) * 1
    );
    loadMonth(currentMonth, content, monthDiv);
  });
});

我们为每个按钮添加一个事件监听器。我们将使用之前创建的data-action属性来判断我们点击的是“下一个”按钮还是“上一个”按钮。data -action 的值要么为 1,要么为 -1。我们修改 currentMonth 变量并再次调用loadMonth 函数,因为我们需要更新日历的内容。

并且它有效!

恭喜,您刚刚创建了一个 Javascript 插件!

以下是完整的 Javascript 代码:

(function (root, factory) {
  root.myCalendar = factory(root);
})(this, (root) => {
  let monthList = new Array(
    "january",
    "february",
    "march",
    "april",
    "may",
    "june",
    "july",
    "august",
    "september",
    "october",
    "november",
    "december"
  );
  let dayList = new Array(
    "sunday",
    "monday",
    "tuesday",
    "wednesday",
    "thursday",
    "friday",
    "saturday"
  );
  let today = new Date();
  today.setHours(0, 0, 0, 0);
  let privateVar = "No, No, No...";

  let init = () => {
    let element = document.getElementById("calendar");

    let currentMonth = new Date(today.getFullYear(), today.getMonth(), 1);

    // Creating the div for our calendar's header
    let header = document.createElement("div");
    header.classList.add("header");
    element.appendChild(header);

    // Creating the div that will contain the days of our calendar
    let content = document.createElement("div");
    element.appendChild(content);

    // Our "previous" button
    let previousButton = document.createElement("button");
    previousButton.setAttribute("data-action", "-1");
    previousButton.textContent = "\u003c";
    header.appendChild(previousButton);

    // Creating the div that will contain the actual month/year
    let monthDiv = document.createElement("div");
    monthDiv.classList.add("month");
    header.appendChild(monthDiv);

    // Our "next" button
    let nextButton = document.createElement("button");
    nextButton.setAttribute("data-action", "1");
    nextButton.textContent = "\u003e";
    header.appendChild(nextButton);

    // Next/previous button functionality
    element.querySelectorAll("button").forEach((element) => {
      element.addEventListener("click", () => {
        console.log(element.getAttribute("data-action"));
        currentMonth.setMonth(
          currentMonth.getMonth() * 1 +
            parseInt(element.getAttribute("data-action")) * 1
        );
        loadMonth(currentMonth, content, monthDiv);
      });
    });

    // Load current month
    loadMonth(currentMonth, content, monthDiv);
  };

  let createDaysNamesCells = (content) => {
    for (let i = 0; i < dayList.length; i++) {
      let cell = document.createElement("span");
      cell.classList.add("cell");
      cell.classList.add("day");
      cell.textContent = dayList[i].substring(0, 3).toUpperCase();
      content.appendChild(cell);
    }
  };

  let createEmptyCellsIfNecessary = (content, date) => {
    for (let i = 0; i < date.getDay(); i++) {
      let cell = document.createElement("span");
      cell.classList.add("cell");
      cell.classList.add("empty");
      content.appendChild(cell);
    }
  };

  let loadMonth = (date, content, monthDiv) => {
    // Empty the calendar
    content.textContent = "";

    // Adding the month/year displayed
    monthDiv.textContent =
      monthList[date.getMonth()].toUpperCase() + " " + date.getFullYear();

    // Creating the cells containing the days of the week
    createDaysNamesCells(content);

    // Creating empty cells if necessary
    createEmptyCellsIfNecessary(content, date);

    // Number of days in the current month
    let monthLength = new Date(
      date.getFullYear(),
      date.getMonth() + 1,
      0
    ).getDate();

    // Creating the cells containing current's month's days
    for (let i = 1; i <= monthLength; i++) {
      let cell = document.createElement("span");
      cell.classList.add("cell");
      cell.textContent = `${i}`;
      content.appendChild(cell);

      // Cell's timestamp
      let timestamp = new Date(
        date.getFullYear(),
        date.getMonth(),
        i
      ).getTime();
      cell.addEventListener("click", () => {
        console.log(timestamp);
        console.log(new Date(timestamp));

        document.querySelector(".cell.today")?.classList.remove("today");
        cell.classList.add("today");
      });

      // Add a special class for today
      if (timestamp === today.getTime()) {
        cell.classList.add("today");
      }
    }
  };
  return {
    init,
  };
});

玩得开心❤️

鏂囩珷鏉ユ簮锛�https://dev.to/damcosset/write-a-calendar-plugin-with-javascript-kk3
PREV
为什么选择 Docker?Docker 到底能做什么?虚拟机不是慢吗?难道我不能直接把应用程序上传到一堆云服务器上吗?这里有一个更简单的解决方案。但这在开发过程中能给我带来什么帮助呢?总结一下……
NEXT
在您准备好之前开始简介在您准备好之前开始那么现在就开始吧...