资讯专栏INFORMATION COLUMN

带你开发一个日历控件

shiina / 1678人阅读

摘要:直接使用事件代理机制,将事件绑定在整个日历的上即可,这样事件只用在创建时初始化一次即可,简单高效省内存。

首发我的博客 - https://blog.cdswyda.com/post/2017121010

日历控件多的不胜枚举,为什么我们还要再造一个轮子呢?

因为大多数日历控件都是用于选择日期的,有种需求是要在日历上展示各种各样的内容,这样的日历控件较少,而且试用下来并不满意。

因此就再造一个轮子,现在带你一起基于使用之前完成的组件机制来开发一个日历控件。

需求

简单把需求整理如下:

月视图

支持在日历中每一天中插入任意的内容

相关点击事件

获取日历当前视图的开始和结束日期

获取设置选中的日期

实现分析

首先我们拿系统中自带的日历观察一下,看看日历的特征到底是怎么样的。

一个月中有 28 到 31 天不等,但是为了保证完整的结构,日历中会有部分上一月和下一月的日期,总结下来,一个月中显示的必定是整整6周的日期。

那么只要得到当月的开始日期就可以绘制日历了。

如何计算当月日历视图中的开始日期呢? 前面已经分析了,为了保证完整,它显示了上一月的部分天数,那么只用从当月的1号开始往前推算就可以了。

开始日期 = 当月1号的日期 - 当月1号的星期
结束日期 = 开始日期 + 42天

这个问题搞清楚了,感觉实现这么一个日历就没什么大阻碍了,开始动工吧!

必要结构准备

首先构建如下所示的基本结构

其中:

头部左右为个性化区域,用于实际使用时放置任意内容。中间用于显示当前月份和切换按钮

主体区域中用绘制整个日历

thead 中绘制周一至周日 或周日至周一的星期,这段内容是不会随月份切换而改变的,可以直接准备好

tbody 中用于绘制可变的日期,准备好容器留空即可。

脚部区域用于实际使用时放置任意各项化内容

menu区域用于切换日期时弹出的面板

绘制日历

在初始化好日历结构后就可以开始绘制日历了。

计算一个月中的开始日期和结束日期

首先完成开始和结束时间的计算

{
    // 初始化当前月份的开始日期和结束日期
    _initStartEnd: function () {
        // 当月1号
        var currMonth = moment(this.currMonth, "YYYY-MM"),
            // 当月1号是周几 the ISO day of the week with 1 being Monday and 7 being Sunday.
            firstDay_weekday = currMonth.isoWeekday(),
            startDateOfMonth,
            endDateOfMonth;
        if (!this.dayStartFromSunday) {
            // 开始为周一 则向前减少周几的天数-1即为 开始的日期
            startDateOfMonth = currMonth.subtract(firstDay_weekday - 1, "day");
        } else {
            // 开始为周日 则直接向前周几的天数即可
            startDateOfMonth = currMonth.subtract(firstDay_weekday, "day");
        }

        endDateOfMonth = startDateOfMonth.clone().add(41, "day");

        this.startDateOfMonth = startDateOfMonth;
        this.endDateOfMonth = endDateOfMonth;
    }
}

由于要处理很多日期,而JavaScript中关于日期处理时,不同浏览器下差异较大,因此直接使用 moment.js 来对日期进行统一处理。

由于使用习惯不同,一周的开始到底是周一还是周日是不确定的,因此直接作为配置即可。

绘制一月中的日期

上面已经计算得到了一个月的开始日期和结束日期,那么只用遍历进行绘制即可。

由于我们使用了表格实现,因此需要按行绘制。

实现如下:

{
    // 日历可变部分的渲染
    _render: function () {
        this._initStartEnd();

        var weeks = 6,
            days = 7,
            curDate = this.startDateOfMonth.clone(),
            tr;

        var start = this.startDateOfMonth.format("YYYY-MM-DD"),
            end = this.endDateOfMonth.format("YYYY-MM-DD");

        // 清空 并开始新的渲染
        this._clearDays();
        this._renderTitle();

        for (var i = 0; i < weeks; ++i) {
            tr = document.createElement("tr");
            tr.className = "ep-calendar-week";
            this._daysBody.appendChild(tr);

            for (var j = 0; j < days; ++j) {
                // 渲染一天 并递增
                this._renderDay(curDate, tr);
                curDate.add(1, "day");
            }
        }
    },
    // 每天的渲染
    _renderDay: function (date, currTr) {
        var td = document.createElement("td"),
            tdInner = document.createElement("div"),
            text = document.createElement("span"),
            day = date.isoWeekday(),
            // 返回的月份是0-11
            month = date.month() + 1;

        tdInner.appendChild(text);
        td.appendChild(tdInner);

        td.className = "ep-calendar-date";
        tdInner.className = "ep-calendar-date-inner";
        // 完整日期
        td.setAttribute("data-date", date.format("YYYY-MM-DD"));
        // 对应的iso星期
        td.setAttribute("data-isoweekday", day);

        // 周末标记text.className
        if (day === 6 || day === 7) {
            td.className += " ep-calenday-weekend";
        }
        // 非本月标记
        // substr 在ie8下有问题
        // if (month != parseInt(this.currMonth.substr(-2))) {
        if (month != parseInt(this.currMonth.substr(5), 10)) {
            td.className += " ep-calendar-othermonth";
        }
        // 今天标记
        if (this.today == date.format("YYYY-MM-DD")) {
            td.className += " ep-calendar-today";
        }

        // 每天渲染时发生 还未插入页面
        var renderEvent = this.fire("cellRender", {
            // 当天的完整日期
            date: date.format("YYYY-MM-DD"),
            // 当天的iso星期
            isoWeekday: day,
            // 日历dom
            el: this.el,
            // 当前单元格
            tdEl: td,
            // 日期文本
            dateText: date.date(),
            // 日期class
            dateCls: "ep-calendar-date-text",
            // 需要注入的额外的html
            extraHtml: "",

            isHeader: false
        });

        // 处理对dayText内容和样式的更改
        text.innerText = renderEvent.dateText;
        text.className = renderEvent.dateCls;

        // 添加新增内容
        if (renderEvent.extraHtml) {
            jQuery(renderEvent.extraHtml).appendTo(tdInner);
        }

        currTr.appendChild(renderEvent.tdEl);

        // 每天渲染后发生 插入到页面
        this.fire("afterCellRender", {
            date: date.format("YYYY-MM-DD"),
            isoWeekday: day,
            el: this.el,
            tdEl: td,
            dateText: text.innerText,
            dateCls: text.className,
            extraHtml: renderEvent.extraHtml,
            isHeader: false
        });
    }
}

直接从开始日期往后依次画出42天即可。

为了灵活性,在绘制的不同时机触发了不同的事件,在使用时可绑定相应的事件,在其中进行个性化操作。

也为了使用了方便和灵活性,直接在绘制日期时,在相应的dom上加入了所对应的日期和星期属性。

在此过程中需要对日期是否周末、是否本月、是否是选中的、是否是今天等进行相应的标记处理。

绘制其他内容

除了上面所述之外此外还要绘制出年月选择、标题等,这些实际就是给已经有的dom元素中更改内容而已,就不再展开了。

切换月份的实现

上面已经基本绘制出了一个日历,切换月份实际就更简单了,只用根据新的月份重新计算开始日期,清空原来的内容,重新进行绘制即可。

{
    // 设置月份
    setMonth: function (ym) {
        var date = moment(ym, "YYYY-MM");

        if (date.isValid()) {
            var oldMonth = this.currMonth,
                aimMonth = date.format("YYYY-MM");

            // 月份变动前
            this.fire("beforeMonthChange", {
                el: this.el,
                oldMonth: oldMonth,
                newMonth: aimMonth
            });

            this.currMonth = aimMonth;
            this.render();

            // 月份变动后
            this.fire("afterMonthChange", {
                el: this.el,
                oldMonth: oldMonth,
                newMonth: aimMonth
            });

        } else {
            throw new Error(ym + "是一个不合法的日期");
        }
    }
}
事件的处理

要处理的事件较多,此处仅仅以日期的点击作为示意。

{
    // 初始化事件
    _initEvent: function () {
        var my = this;
        jQuery(this.el)
            // 日期单元格
            .on("click", ".ep-calendar-date", function (e) {
                var date = this.getAttribute("data-date"),
                    ev = my.fire("dayClick", {
                        ev: e,
                        date: date,
                        day: this.getAttribute("data-isoweekday"),
                        el: my.el,
                        tdEl: this
                    });

                // 如果修改事件对象的cancel为true后 则不进行后续的选中操作
                if (!ev.cancel) {
                    my.setSelected(date);
                }
            })
    }
}

由于日期所对应的dom元素始终会添加和移除,直接把事件绑定在日期的dom元素上,则必须在每次新增后重新绑定事件,十分麻烦。

直接使用事件代理机制,将事件绑定在整个日历的dom上即可,这样事件只用在创建时初始化一次即可,简单、高效、省内存。

使用

我们新增这个控件的主要目的就是要支持在日历中绘制任意内容,怎么使用呢?

var testCalendar = epctrl.init("Calendar", {
    el: "#date",
    // 资源加载过程中的事件需要直接在这里指定
    events: {
        beforeSourceLoad: function (e) {
            // 资源加载前,在加入我们的皮肤样式文件
            e.cssUrl.push("./test-skin.css");
        }
    }
});
// 日期部分渲染前 支持动态获取数据
testCalendar.on("beforeDateRender", function (e) {
    var startDate = e.startDate,
        endDate = e.endDate;
    // 如果需要动态获取数据
    // 则将获取数据的ajax加到事件对象的ajax属性上即可
    // 日期渲染的cellRender事件将在ajax成功获取数据后执行
    e.ajax = $.ajax({
        url: "getDateInfo.xxx",
        // 将当月视图的开始和结束时间传递过去
        data: {
            start: startDate,
            end: endDate
        }
    });
});
// 控制渲染过程 可插入任意内容或修改原来的内容
testCalendar.on("cellRender", function (e) {
    if (!e.isHeader) {
        // 如:周五周六则插入周末 否则插入工作日
        e.extraHtml = "
" + (e.isoWeekday > 5 ? "周末": "工作日") + "
"; } });
总结

以上就是关于一个月视图日历控件核心步骤了。

此日历实现基于一个控件基类扩展而来,其必要功能仅为一套事件机制,可参考实现一套自定义事件机制

上面只分析了关键步骤,和核心代码,为了方便使用和扩展性,实际代码中还要处理很多问题。源码和文档如下,感兴趣可以阅读:月视图日历

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92055.html

相关文章

  • 带你开发一个二维周视图日历

    摘要:即之前实现了一个月视图日历,我们今天来实现一个二维周视图的日历。难点实现内容部件插入我们实现这个二维周视图日历的主要目的就是要支持插入任意的内容,上面已经准备好了插入内容的元素,这里要做的就是将数据绘制成放置在合适的位置。 即之前实现了一个月视图日历,我们今天来实现一个二维周视图的日历。 以下进行分析其中的关键部分。 结构准备 不同之处在于其在日历的基础上还有一个分类轴,用于展示不同的...

    张金宝 评论0 收藏0
  • 一款基于移动端的日历控件iantooDate

    摘要:已被移除过时的提醒时间的颜色。默认当日历控件滑动的时候是否实时更新日历控件的位置主要是对部分低端机型做性能处理。返回当前的时间调用该方法关闭日历控件。年月日时分秒星期 原文链接 使用 引入文件: ./build/css/iantooDate.css ./build/js/iantooDate.js 并在页面上调用: iantoo.date() 详细使用方法见page/iantooDat...

    liangzai_cool 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<