├── .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 | "
" + 185 | "" + 186 | "
" + 187 | "" + 190 | "" + 193 | "
" + 194 | "" + 195 | "
" + 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 = ""; 330 | 331 | 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 | --------------------------------------------------------------------------------