├── .gitignore
├── README.md
├── calendar.css
├── calendar.js
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DatePicker
2 |
3 | Without jQuery-based date component with support for date ranges, muitiple calendars and more.
4 |
5 | ## Params
6 |
7 | - el:
8 | - type: **Element**
9 | - required: `true`
10 | - description: Calendar container
11 | - trigger:
12 | - type: **Element**
13 | - required: `true`
14 | - description: Provoke DatePicker show and hide
15 | - eg: `input element`(onfocus show the DatePicker)
16 | - default:
17 | - type: **String**
18 | - default: `'today'`
19 | - eg: `'2017-04-15'`
20 | - description: Default selected date
21 | - isRadio:
22 | - type: **Boolean**
23 | - default: `false`
24 | - description: Is it date radio selected?
25 | - lang:
26 | - type: **String**
27 | - options: `'EN'` | `'CN'`
28 | - default: `'EN'`
29 | - description: The font language, for `CN` will use Chinese, for `EN` will use English
30 | - position:
31 | - type: **String**
32 | - options: `'top'` | `'right'` | `'left'` | `'bottom'`
33 | - default: `'bottom'`
34 | - interval:
35 | - type: **Array**
36 | - default: `[1970, 2030]`
37 | - eg:`[2000, 2020]`
38 | - description: Select abled range
39 | - showFn:
40 | - type: **Function**
41 | - description: After show callback
42 | - hideFn:
43 | - type: **Function**
44 | - description: After hide callback
45 | - onchange:
46 | - type: **Function**
47 | - description: callback which observe datePicker change
48 |
49 | ## Methods
50 |
51 | - show:
52 | - type: **Function**
53 | - hide:
54 | - type: **Function**
55 |
56 | ## Usage
57 |
58 | 1. manually import `calendar.css` and `calendar.js`
59 | 2. create DatePicker contructor, for below:
60 | ```html
61 |
62 |
63 | ```
64 |
65 |
66 | ```js
67 | var dateComponent = new DatePicker({
68 | el: document.querySelector('#calendar'),
69 | onchange: function (curr) {
70 | dateInput.value = curr;
71 | }
72 | });
73 |
74 | var dateInput = document.querySelector('#datePicker');
75 |
76 | dateInput.onfocus = function () {
77 | dateComponent.show();
78 | };
79 | ```
80 |
81 |
--------------------------------------------------------------------------------
/calendar.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .calendar-wrapper {
7 | max-width: 310px;
8 | }
9 |
10 | .calendar-header {
11 | position: relative;
12 | background-color: #c61b01;
13 | padding: 10px 0;
14 | overflow: auto;
15 | }
16 |
17 | .calendar-mid {
18 | position: absolute;
19 | left: 50%;
20 | -webkit-transform: translate(-50%);
21 | transform: translate(-50%);
22 | }
23 |
24 | .calendar-header .leftArrow {
25 | width: 0;
26 | height: 0;
27 | border-width: 10px;
28 | border-style: solid;
29 | border-top-color: transparent;
30 | border-bottom-color: transparent;
31 | border-left-color: transparent;
32 | border-right-color: #ffffff;
33 | margin-left: 5px;
34 | float: left;
35 | cursor: pointer;
36 | }
37 |
38 | .calendar-header .rightArrow {
39 | width: 0;
40 | height: 0;
41 | border-width: 10px;
42 | border-style: solid;
43 | border-top-color: transparent;
44 | border-bottom-color: transparent;
45 | border-left-color: #fff;
46 | border-right-color: transparent;
47 | margin-left: 5px;
48 | float: right;
49 | cursor: pointer;
50 | }
51 |
52 | ul.weekTip, ul.date {
53 | font-size: 0;
54 | }
55 |
56 | ul.weekTip > li, ul.date > li {
57 | display: inline-block;
58 | box-sizing: border-box;
59 | font-size: 15px;
60 | color: #000000;
61 | width: 40px;
62 | height: 40px;
63 | border-radius:50%;
64 | line-height: 40px;
65 | text-align: center;
66 | cursor: pointer;
67 | margin-bottom: 5px;
68 | }
69 |
70 | ul.weekTip > li:not(:nth-of-type(7n)), ul.date > li:not(:nth-of-type(7n)) {
71 | margin-right: 5px;
72 | }
73 |
74 | ul.date > li.active {
75 | background-color: rgb(200, 17, 1);
76 | color: #ffffff;
77 | }
78 |
79 | ul.date > li.across {
80 | border: 1px solid rgb(200, 17, 1);
81 | color: rgb(200, 17, 1);
82 | }
83 |
84 | ul > li.weekend {
85 | color: rgb(200, 17, 1);
86 | }
87 |
88 | ul > li.disabled {
89 | color: lightgray;
90 | }
91 |
92 | #demo {
93 | position: relative;}
--------------------------------------------------------------------------------
/calendar.js:
--------------------------------------------------------------------------------
1 | var DatePicker = (function () {
2 |
3 | /**
4 | * 日历组件
5 | * @param config {target: DOM, defaultDate: '2017-04-15', selectInterval: [2000, 2020]}
6 | * @constructor
7 | */
8 | function Calendar(config) {
9 |
10 | this.target = function () {
11 | if (config.el) {
12 | return config.el;
13 | } else {
14 | throw "The el option is required";
15 | }
16 | }();
17 | this.trigger = function () {
18 | if (config.trigger) {
19 | return config.trigger;
20 | } else {
21 | throw "The trigger option is required";
22 | }
23 | }();
24 | this.defaultDate = config.default || 'today';
25 | this.isRadio = !!(config.isRadio);
26 | this.lang = (config.lang == 'CN') ? config.lang : 'EN';
27 | this.position = config.position || 'bottom';
28 | this.selectInterval = config.interval || [1970, 2030];
29 | this.isShow = true;
30 |
31 | this.showFn = config.showFn || function () {
32 | };
33 | this.hideFn = config.hideFn || function () {
34 | };
35 | this.onchange = config.onchange || function () {
36 | };
37 | // 当前日历数据
38 | this.nonceYear = 0;
39 | this.nonceMonth = 0;
40 | this.nonceDay = 0;
41 |
42 | // 选中的数据
43 | this.selectedDate = {
44 | begin: {
45 | year: 0,
46 | month: 0,
47 | day: 0
48 | },
49 | end: {
50 | year: 0,
51 | month: 0,
52 | day: 0
53 | }
54 | };
55 | // 日期数组
56 | this.renderDate = [];
57 | this.init();
58 | }
59 |
60 | // DatePicker = Calendar;
61 | var utils = {
62 | // 惰性载入
63 | bind: function (el, event, listener) {
64 | if (el.addEventListener) {
65 | this.bind = function (el, event, listener) {
66 | el.addEventListener(event, listener);
67 | }
68 | } else if (el.attachEvent) {
69 | this.bind = function (el, event, listener) {
70 | el.attachEvent.call(el, event, listener);
71 | }
72 | } else {
73 | this.bind = function (el, event, listener) {
74 | el.on[event] = listener;
75 | }
76 | }
77 | return this.bind.apply(this, arguments);
78 | },
79 | delegates: function (tagName, fn) {
80 | return function (e) {
81 | var event = e || window.event,
82 | target = event.target || event.srcElement;
83 | if (target.tagName.toLowerCase() === tagName) {
84 | fn.call(null, target);
85 | }
86 | }
87 | },
88 |
89 | addClass: function (el, className) {
90 |
91 | var ol = el.className;
92 | if (ol.split(' ').indexOf(className) === -1) {
93 | el.className = ol + " " + className;
94 | }
95 |
96 | },
97 |
98 | removeClass: function (el, className) {
99 | ([]).forEach.call(el, function (ele) {
100 |
101 | var ol = ele.className,
102 | reg = new RegExp("(\\s)*" + className);
103 | ele.className = ol.replace(reg, '');
104 | });
105 |
106 |
107 | }
108 | }
109 |
110 | Calendar.prototype = {
111 | constructor: Calendar,
112 |
113 | show: function () {
114 | this.target.style.display = 'inline-block';
115 | this.isShow = true;
116 | this.target.style.position = 'absolute';
117 |
118 | var triggerW = this.trigger.offsetWidth,
119 | triggerH = this.trigger.offsetHeight,
120 | offTop = this.trigger.offsetTop,
121 | offLeft = this.trigger.offsetLeft;
122 |
123 | var targetW = this.target.offsetWidth,
124 | targetH = this.target.offsetHeight;
125 | var leftAttr, topAttr;
126 | if (this.position == 'top') {
127 | leftAttr = offLeft;
128 | topAttr = offTop - targetH;
129 | } else if (this.position == 'right') {
130 | leftAttr = offLeft + triggerW;
131 | topAttr = offTop;
132 | } else if (this.position == 'left') {
133 | leftAttr = offLeft - targetW;
134 | topAttr = offTop;
135 | } else {
136 | leftAttr = offLeft;
137 | topAttr = offTop + triggerH;
138 | }
139 | this.target.style.left = leftAttr + 'px';
140 | this.target.style.top = topAttr + 'px';
141 | this.showFn();
142 | },
143 |
144 | hide: function () {
145 | this.target.style.display = 'none';
146 | this.isShow = false;
147 | this.hideFn();
148 | },
149 |
150 | get: function () {
151 | var self = this;
152 |
153 | function format(attr) {
154 | // 结束日期为空时,不显示
155 | if (attr == 'end'
156 | && !self.selectedDate[attr].year) {
157 | return;
158 | }
159 | return self.selectedDate[attr].year +
160 | '-' +
161 | (self.selectedDate[attr].month + 1) +
162 | '-' +
163 | self.selectedDate[attr].day;
164 | }
165 |
166 | var currBegin = format('begin')
167 | , currEnd = format('end');
168 | if (this.isRadio) {
169 | return currBegin;
170 | } else {
171 | return [currBegin, currEnd];
172 | }
173 | },
174 | /**
175 | * 初始化
176 | */
177 | init: function () {
178 | this.hide();
179 | // compute select year
180 | var yearOpts = this._productOptions(this.selectInterval, this.lang == 'CN' ? '年' : '');
181 | var dateOpts = this._productOptions([1, 12], this.lang == 'CN' ? '月' : '');
182 | // 初始化布局
183 | var navTop = "
" +
184 | "" +
196 | "
";
197 |
198 | this.target.innerHTML = navTop;
199 |
200 | this._setDefault();
201 |
202 | this.eventListener();
203 | },
204 | /**
205 | * 生成选项
206 | * @param between
207 | * @param concat
208 | * @returns {string}
209 | * @private
210 | */
211 | _productOptions: function (between, concat) {
212 | var opts = "";
213 | for (var i = between[0]; i <= between[1]; i++) {
214 | opts += "";
217 | }
218 | return opts;
219 | },
220 | /**
221 | * 设置默认的日期
222 | * @private
223 | */
224 | _setDefault: function () {
225 | var defaultD = this.defaultDate,
226 | self = this;
227 | if (defaultD === 'today') {
228 | var d = new Date();
229 | self.nonceYear = d.getFullYear();
230 | self.nonceMonth = d.getMonth();
231 | self.nonceDay = d.getDate();
232 | } else {
233 | var relate = defaultD.split('-');
234 | if (relate.length <= 1)
235 | throw "default option is invalid, please check the options and reset `YYYY-mm-dd` or `YYYY-mm`";
236 | else {
237 | self.nonceYear = relate[0];
238 | self.nonceMonth = +relate[1] - 1;
239 | self.nonceDay = relate[2] || 1;
240 | }
241 | }
242 | // 设置选中的默认年份与月份
243 | self.selectedDate.begin.year = self.nonceYear;
244 | self.selectedDate.begin.month = self.nonceMonth;
245 | self._setDateList();
246 |
247 |
248 | },
249 |
250 | /**
251 | * 处理日期数组
252 | * @returns {Array}
253 | * @private
254 | */
255 | _setDateList: function () {
256 | /*一个月的第一天星期几*/
257 | var whatday = new Date(this.nonceYear, this.nonceMonth, 1).getDay();
258 |
259 | var monthMuchDay = [31,
260 | (this.nonceYear % 4 === 0 && (this.nonceYear % 100 !== 0 || this.nonceYear % 400 === 0))
261 | ? 29 : 28
262 | , 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
263 |
264 | var renderList = [],
265 | /*4/16 Fixed 当前月为1月份,手动设置上一个月为12月份*/
266 | prevMonthDays = this.nonceMonth - 1 > 0 ? monthMuchDay[this.nonceMonth - 1] : 31,
267 | nonceMonthDays = monthMuchDay[this.nonceMonth];
268 | /*一个月的最后一天星期几*/
269 | var monthLastDay = new Date(this.nonceYear, this.nonceMonth, nonceMonthDays).getDay();
270 |
271 | /*前置补空*/
272 | var prevOnce = whatday, obj;
273 | while (prevOnce > 0) {
274 | obj = {
275 | isInner: false,
276 | num: prevMonthDays--
277 | };
278 | prevOnce--;
279 | renderList.unshift(obj);
280 | }
281 | /*当前月天数*/
282 | for (var i = 1; i <= nonceMonthDays; i++) {
283 | obj = {
284 | isInner: true,
285 | num: i
286 | };
287 | renderList.push(obj);
288 | }
289 | /*后置补空*/
290 | for (var i = 1; monthLastDay < 6; monthLastDay++, i++) {
291 | obj = {
292 | isInner: false,
293 | num: i
294 | };
295 | renderList.push(obj);
296 | }
297 | this.renderDate = renderList;
298 |
299 | this._doRender(renderList);
300 | },
301 | /**
302 | * 根据日期数组渲染DOM
303 | * @param arr
304 | * @private
305 | */
306 | _doRender: function (arr) {
307 |
308 | var self = this,
309 | lang = {
310 | 'EN': ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'],
311 | 'CN': ['日', '一', '二', '三', '四', '五', '六']
312 | },
313 | label = lang[self.lang].map(function (ele, i) {
314 | if (i == 0 || lang.length - 1) {
315 | return "" + ele + ""
316 | } else {
317 | return "" + ele + ""
318 | }
319 | }),
320 | selectedBgTs = +new Date(self.selectedDate.begin.year,
321 | self.selectedDate.begin.month,
322 | self.selectedDate.begin.day),
323 | selectedEndTs = +new Date(self.selectedDate.end.year,
324 | self.selectedDate.end.month,
325 | self.selectedDate.end.day);
326 |
327 | var date = "" +
328 | label.join('') +
329 | "
";
330 |
331 | date += "";
332 |
333 |
334 | arr.forEach(function (ele, ind) {
335 | var line = Math.floor(ind / 6)
336 | , currTs = +new Date(self.nonceYear, self.nonceMonth, ele.num);
337 | // 非这个月的日子
338 | if (!ele.isInner)
339 | date += "- " + ele.num + "
";
340 | // 选中的开始日子
341 | else if (ele.num === self.selectedDate.begin.day &&
342 | self.nonceMonth === self.selectedDate.begin.month &&
343 | self.nonceYear === self.selectedDate.begin.year)
344 | date += "- " + ele.num + "
";
345 | // 选中的结束日子
346 | else if (ele.num === self.selectedDate.end.day &&
347 | self.nonceMonth === self.selectedDate.end.month &&
348 | self.nonceYear === self.selectedDate.end.year)
349 | date += "- " + ele.num + "
";
350 | // 间隔的日子
351 | else if (currTs > selectedBgTs && currTs < selectedEndTs)
352 | date += "- " + ele.num + "
";
353 | // 周末的日子
354 | else if (!(ind % 7) || ind === line * 6 + line - 1)
355 | date += "- " + ele.num + "
";
356 | // 未选中的日子
357 | else
358 | date += "- " + ele.num + "
";
359 | });
360 |
361 | date += "
";
362 |
363 | var dateContainer = document.createElement('div');
364 | dateContainer.className = 'dateWrap';
365 | dateContainer.innerHTML = date;
366 |
367 | var header = this.target.querySelector(" .calendar-header");
368 | // add
369 | var fload;
370 | if (fload = (header.nextElementSibling)) {
371 | header.parentNode.replaceChild(dateContainer, fload);
372 | } else {
373 | header.parentNode.insertBefore(dateContainer, null);
374 | }
375 |
376 | this._selected(" .year option", this.nonceYear);
377 | this._selected(" .date option", this.nonceMonth + 1);
378 | },
379 | /**
380 | * 默认选中年份和月份选择框
381 | * 2017/07/21 fixed
382 | * `
383 | * `
384 | * 先removeAttribute(2012),再setAttribute(2013)。2013不能被选中,改用opt.select=Boolean
385 | * @param selector
386 | * @param tag
387 | * @private
388 | */
389 | _selected: function (selector, tag) {
390 | var options = this.target.querySelectorAll(selector);
391 | ([]).forEach.call(options, function (opt) {
392 | opt.selected = opt.value === tag.toString()
393 | });
394 | },
395 |
396 | /**
397 | * 上一个月的操作
398 | * @private
399 | */
400 | _prevMonth: function () {
401 | if (this.nonceMonth === 0) {
402 | this.nonceYear--;
403 | this.nonceMonth = 11;
404 | } else {
405 | this.nonceMonth--;
406 | }
407 | this._setDateList();
408 | },
409 |
410 | /**
411 | * 下一个月的操作
412 | * @private
413 | */
414 | _nextMonth: function () {
415 | if (this.nonceMonth === 11) {
416 | this.nonceYear++;
417 | this.nonceMonth = 0;
418 | } else {
419 | this.nonceMonth++;
420 | }
421 | this._setDateList();
422 | },
423 |
424 | /**
425 | * 各种事件监听
426 | */
427 | eventListener: function () {
428 | var self = this;
429 | /**
430 | * 左箭头监听
431 | */
432 | utils.bind(self.target.querySelector(".leftArrow"),
433 | 'click',
434 | function () {
435 | self._prevMonth();
436 | });
437 |
438 | /**
439 | * 右箭头监听
440 | */
441 | utils.bind(self.target.querySelector(".rightArrow"),
442 | 'click',
443 | function () {
444 | self._nextMonth();
445 | });
446 |
447 | /**
448 | * 日期点击监听
449 | * 2017/4/14 fixed:用事件冒泡的方式监听动态添加元素的绑定事件
450 | */
451 | utils.bind(document,
452 | 'click',
453 | utils.delegates(
454 | 'li',
455 | function (t) {
456 | // 父元素不是ul.date时跳出
457 | if (!t.parentNode.classList.contains('date')) return;
458 |
459 | if (self.isRadio) {
460 | selectRadio(self, 'begin', t);
461 | } else {
462 | selectRange(self, t);
463 | }
464 |
465 | self._setDateList();
466 |
467 | self.onchange(self.get());
468 |
469 | // 判断是否为范围选择
470 | if (!self.isRadio && self.selectedDate.end.year) {
471 | self.hide();
472 | } else if (self.isRadio) {
473 | self.hide();
474 | }
475 | }
476 | )
477 | );
478 |
479 | // 选一个日期时
480 | function selectRadio(oDate, type, target) {
481 | oDate.selectedDate[type].day = +(target.innerHTML);
482 | // 点击的是非本月的日期
483 | var clkId = [].indexOf.call(target.parentElement.children, target);
484 | if (!oDate.renderDate[clkId].isInner) {
485 | if (oDate.selectedDate[type].day > 15) {
486 | oDate.selectedDate[type].month = (oDate.nonceMonth - 1 < 0) ? 11 : oDate.nonceMonth - 1;
487 | oDate._prevMonth();
488 | } else {
489 | oDate.selectedDate[type].month = (oDate.nonceMonth + 1 >= 12) ? 0 : oDate.nonceMonth + 1;
490 | oDate._nextMonth();
491 | }
492 | } else {
493 | oDate.selectedDate[type].month = oDate.nonceMonth;
494 | }
495 |
496 | oDate.selectedDate[type].year = oDate.nonceYear;
497 | }
498 |
499 | // 选日期范围
500 | function selectRange(oDate, target) {
501 | var oSelected = self.selectedDate
502 | , isSelEnd = (!!oSelected.end.year)
503 | , isSelBg = (!!oSelected.begin.day)
504 | , day = +(target.innerHTML);
505 |
506 | // 选中的日期是否在开始日期前面
507 | function isSelFront() {
508 | // 选择下一个月的日期时
509 | if (/disabled/.test(target.className)) {
510 | return !(day < 15);
511 | } else {
512 | return (
513 | +new Date(self.nonceYear, self.nonceMonth, day)
514 | < +new Date(oSelected.begin.year, oSelected.begin.month, oSelected.begin.day)
515 | )
516 | }
517 | }
518 |
519 | // 未选择起始时间 || 将选择的时间在begin前面
520 | if (!isSelBg || (!isSelEnd && isSelFront())) {
521 | selectRadio.call(null, oDate, 'begin', target)
522 | }
523 | // 未选择截止时间
524 | else if (isSelBg && !isSelEnd) {
525 | selectRadio.call(null, oDate, 'end', target)
526 | } else {
527 | selectRadio.call(null, oDate, 'begin', target)
528 | oSelected.end.year = 0;
529 | }
530 |
531 | }
532 |
533 | /**
534 | * 年份选择框监听
535 | */
536 | utils.bind(self.target.querySelector("select.year"),
537 | 'change',
538 | function (e) {
539 | self.nonceYear = e.target.value;
540 | self._setDateList();
541 |
542 | });
543 |
544 | /**
545 | * 月份选择框监听
546 | */
547 | utils.bind(self.target.querySelector("select.date"),
548 | 'change',
549 | function (e) {
550 | self.nonceMonth = e.target.value - 1;
551 | self._setDateList();
552 |
553 | });
554 |
555 | /**
556 | * 点击非日历处隐藏日历
557 | */
558 | utils.bind(document,
559 | 'click',
560 | function (e) {
561 | var showDP = self.isShow,
562 | t = e.target,
563 | breakReg = /dateWrap|calendar\-wrapper/;
564 | while (t) {
565 | if (t == self.trigger || t.className && t.className.search(breakReg) != -1) {
566 | showDP = false;
567 | break;
568 | }
569 | t = t.parentNode
570 | }
571 | if (showDP) {
572 | self.hide();
573 | return true;
574 | }
575 | })
576 | }
577 | }
578 | return Calendar;
579 | })();
580 |
581 | !window.DatePicker && (window.DatePicker = DatePicker);
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Calendar Component
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
34 |
35 |
--------------------------------------------------------------------------------