├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── bower.json ├── date-range-picker.css ├── date-range-picker.min.css ├── dist ├── date-range-picker.js ├── date-range-picker.min.js ├── date-range-picker.min.js.map ├── tiny-date-picker.js ├── tiny-date-picker.min.js └── tiny-date-picker.min.js.map ├── docs ├── date-range-picker.md └── tiny-date-picker.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── readme.md ├── src ├── date-picker-options.js ├── date-range-picker.js ├── index.js ├── lib │ ├── date-manip.js │ ├── dom.js │ ├── emitter.js │ └── fns.js ├── mode │ ├── base-mode.js │ ├── dropdown-mode.js │ ├── index.js │ ├── modal-mode.js │ └── permanent-mode.js └── views │ ├── day-picker.js │ ├── month-picker.js │ └── year-picker.js ├── test ├── .eslintrc.json ├── browser.test.js ├── date-manip.test.js ├── date-picker-options.test.js ├── date-range.html ├── emitter.test.js ├── fns.test.js └── index.html ├── tiny-date-picker.css └── tiny-date-picker.min.css /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 3, 4 | "sourceType": "module" 5 | }, 6 | "env": { 7 | "node": false, 8 | "browser": true, 9 | "es6": false 10 | }, 11 | "rules": { 12 | "no-tabs": 2, 13 | "quotes": [2, "single", "avoid-escape"], 14 | "semi": 2, 15 | "no-undef": 2, 16 | "no-unused-vars": 2, 17 | "array-bracket-spacing": [2, "never"], 18 | "block-scoped-var": 2, 19 | "brace-style": [2, "1tbs"], 20 | "camelcase": 1, 21 | "computed-property-spacing": [2, "never"], 22 | "curly": 2, 23 | "eol-last": 2, 24 | "eqeqeq": [2, "smart"], 25 | "max-depth": [1, 3], 26 | "max-len": [1, 80], 27 | "max-statements": [1, 15], 28 | "new-cap": 0, 29 | "no-extend-native": 2, 30 | "no-mixed-spaces-and-tabs": 2, 31 | "no-trailing-spaces": 2, 32 | "no-use-before-define": [2, "nofunc"], 33 | "object-curly-spacing": [2, "never"], 34 | "keyword-spacing": [2, {"before": true, "after": true}], 35 | "space-unary-ops": 2 36 | } 37 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | shots 3 | *.log 4 | *.jar 5 | todo.md -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example 3 | .eslintrc.json 4 | index.html 5 | .gitignore 6 | node_modules 7 | bower_components 8 | readme.md 9 | bower.json 10 | .travis.yml 11 | jsconfig.json 12 | *.jar 13 | wdio.mocha.conf.js -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "stylelint.enable": true, 3 | "css.validate": false, 4 | "eslint.enable" : true 5 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-date-picker", 3 | "main": [ 4 | "tiny-date-picker.min.css", 5 | "tiny-date-picker.min.js" 6 | ], 7 | "version": "3.2.8", 8 | "homepage": "https://github.com/chrisdavies/tiny-date-picker", 9 | "authors": [ 10 | "Chris Davies " 11 | ], 12 | "description": "A small, zero-dependency date picker", 13 | "keywords": [ 14 | "date", "datepicker", "calendar", "picker" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | ".gitignore", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests", 23 | "__tests__", 24 | "readme.md" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /date-range-picker.css: -------------------------------------------------------------------------------- 1 | .dr-cals { 2 | display: flex; 3 | justify-content: space-between; 4 | background: white; 5 | box-shadow: 8px 8px 32px -16px rgba(0, 0, 0, 0.5); 6 | padding-top: 6px; 7 | position: relative; 8 | border-radius: 6px; 9 | overflow: hidden; 10 | } 11 | 12 | .dr-cals:before { 13 | content: ' '; 14 | height: 6px; 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | background: #3B99FC; 20 | background: linear-gradient(-90deg, #3B99FC 0%, #8AEFC8 100%); 21 | } 22 | 23 | .dr-cals .dp-edge-day { 24 | visibility: hidden; 25 | } 26 | 27 | .dr-cals .dp-cal-footer { 28 | display: none; 29 | } 30 | 31 | .dr-cals .dp { 32 | border: 0; 33 | } 34 | 35 | .dr-cals .dp-permanent { 36 | max-width: 300px; 37 | } 38 | 39 | .dr-cals .dp-selected:focus, 40 | .dr-cals .dp-selected, 41 | .dr-cals .dp-current:focus, 42 | .dr-cals .dp-current { 43 | background: transparent; 44 | color: inherit; 45 | border-radius: 0; 46 | } 47 | 48 | .dr-cals .dp-day-disabled, 49 | .dr-cals .dp-day-disabled:focus { 50 | color: #DDD; 51 | } 52 | 53 | .dr-cal-end .dp { 54 | border-left: 8px solid #F5F5F5; 55 | } 56 | 57 | .dr-cal-start .dp-next, 58 | .dr-cal-end .dp-prev { 59 | visibility: hidden; 60 | } 61 | 62 | .dr-cals .dp-current:hover, 63 | .dr-cals .dr-in-range:hover, 64 | .dr-cals .dr-in-range:focus, 65 | .dr-cals .dr-in-range { 66 | background: #75BCFC; 67 | color: white; 68 | border-radius: 0; 69 | } 70 | 71 | .dr-cals .dr-selected:hover, 72 | .dr-cals .dr-selected:focus, 73 | .dr-cals .dr-selected { 74 | background: #3B99FC; 75 | color: white; 76 | border-radius: 0; 77 | } 78 | 79 | @media (max-width: 616px), (max-height: 480px) { 80 | .dr-cal-end { 81 | display: none; 82 | } 83 | 84 | .dr-cal-start .dp-next { 85 | visibility: visible; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /date-range-picker.min.css: -------------------------------------------------------------------------------- 1 | .dr-cals{display:flex;justify-content:space-between;background:white;box-shadow:8px 8px 32px -16px rgba(0,0,0,0.5);padding-top:6px;position:relative;border-radius:6px;overflow:hidden}.dr-cals:before{content:' ';height:6px;position:absolute;top:0;left:0;right:0;background:#3b99fc;background:linear-gradient(-90deg,#3b99fc 0,#8aefc8 100%)}.dr-cals .dp-edge-day{visibility:hidden}.dr-cals .dp-cal-footer{display:none}.dr-cals .dp{border:0}.dr-cals .dp-permanent{max-width:300px}.dr-cals .dp-selected:focus,.dr-cals .dp-selected,.dr-cals .dp-current:focus,.dr-cals .dp-current{background:transparent;color:inherit;border-radius:0}.dr-cals .dp-day-disabled,.dr-cals .dp-day-disabled:focus{color:#DDD}.dr-cal-end .dp{border-left:8px solid #f5f5f5}.dr-cal-start .dp-next,.dr-cal-end .dp-prev{visibility:hidden}.dr-cals .dp-current:hover,.dr-cals .dr-in-range:hover,.dr-cals .dr-in-range:focus,.dr-cals .dr-in-range{background:#75bcfc;color:white;border-radius:0}.dr-cals .dr-selected:hover,.dr-cals .dr-selected:focus,.dr-cals .dr-selected{background:#3b99fc;color:white;border-radius:0}@media(max-width:616px),(max-height:480px){.dr-cal-end{display:none}.dr-cal-start .dp-next{visibility:visible}} 2 | -------------------------------------------------------------------------------- /dist/date-range-picker.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.DateRangePicker={})}(this,function(t){"use strict";function o(){var t=new Date;return t.setHours(0,0,0,0),t}function h(t,e){return(t&&t.toDateString())===(e&&e.toDateString())}function v(t,e,n){var a=(t=new Date(t)).getDate(),o=t.getMonth()+e;return t.setDate(1),t.setMonth(n?(12+o)%12:o),t.setDate(a),t.getDate()=t}),t.min=e(t.min||i(o(),-100)),t.max=e(t.max||i(o(),100)),t.hilightedDate=t.parse(t.hilightedDate),t}var m={left:37,up:38,right:39,down:40,enter:13,esc:27};function D(t,e,n){return e.addEventListener(t,n,!0),function(){e.removeEventListener(t,n,!0)}}var y=function(){var t=window.CustomEvent;"function"!=typeof t&&((t=function(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n}).prototype=window.Event.prototype);return t}();var b={day:{onKeyDown:function(t,e){var n=t.keyCode,a=n===m.left?-1:n===m.right?1:n===m.up?-7:n===m.down?7:0;n===m.esc?e.close():a&&(t.preventDefault(),e.setState({hilightedDate:(o=e.state.hilightedDate,r=a,(o=new Date(o)).setDate(o.getDate()+r),o)}));var o,r},onClick:{"dp-day":function(t,e){e.setState({selectedDate:new Date(parseInt(t.target.getAttribute("data-date")))})},"dp-next":function(t,e){var n=e.state.hilightedDate;e.setState({hilightedDate:v(n,1)})},"dp-prev":function(t,e){var n=e.state.hilightedDate;e.setState({hilightedDate:v(n,-1)})},"dp-today":function(t,e){e.setState({selectedDate:o()})},"dp-clear":function(t,e){e.setState({selectedDate:null})},"dp-close":function(t,e){e.close()},"dp-cal-month":function(t,e){e.setState({view:"month"})},"dp-cal-year":function(t,e){e.setState({view:"year"})}},render:function(r){var i=r.opts,t=i.lang,e=r.state,n=t.days,a=i.dayOffset||0,s=e.selectedDate,u=e.hilightedDate,d=u.getMonth(),c=o().getTime();return'
'+n.map(function(t,e){return''+n[(e+a)%n.length]+""}).join("")+function(t,e,n){var a="",o=new Date(t);o.setDate(1),o.setDate(1-o.getDay()+e),e&&o.getDate()===e+1&&o.setDate(e-6);for(var r=0;r<42;++r)a+=n(o),o.setDate(o.getDate()+1);return a}(u,a,function(t){var e=t.getMonth()!==d,n=!i.inRange(t),a=t.getTime()===c,o="dp-day";return o+=e?" dp-edge-day":"",o+=h(t,u)?" dp-current":"",o+=h(t,s)?" dp-selected":"",o+=n?" dp-day-disabled":"",o+=a?" dp-day-today":"",'"})+'
"}},year:{render:function(t){var e=t.state,n=e.hilightedDate.getFullYear(),a=e.selectedDate.getFullYear();return'
'+function(t,e){for(var n="",a=t.opts.max.getFullYear();a>=t.opts.min.getFullYear();--a)n+=e(a);return n}(t,function(t){var e="dp-year";return e+=t===n?" dp-current":"",'"})+"
"},onKeyDown:function(t,e){var n=t.keyCode,a=e.opts,o=n===m.left||n===m.up?1:n===m.right||n===m.down?-1:0;if(n===m.esc)e.setState({view:"day"});else if(o){t.preventDefault();var r=i(e.state.hilightedDate,o);e.setState({hilightedDate:l(r,a.min,a.max)})}},onClick:{"dp-year":function(t,e){e.setState({hilightedDate:(n=e.state.hilightedDate,a=parseInt(t.target.getAttribute("data-year")),(n=new Date(n)).setFullYear(a),n),view:"day"});var n,a}}},month:{onKeyDown:function(t,e){var n=t.keyCode,a=n===m.left?-1:n===m.right?1:n===m.up?-3:n===m.down?3:0;n===m.esc?e.setState({view:"day"}):a&&(t.preventDefault(),e.setState({hilightedDate:v(e.state.hilightedDate,a,!0)}))},onClick:{"dp-month":function(t,e){e.setState({hilightedDate:(n=e.state.hilightedDate,a=parseInt(t.target.getAttribute("data-month")),v(n,a-n.getMonth())),view:"day"});var n,a}},render:function(t){var e=t.opts.lang.months,a=t.state.hilightedDate.getMonth();return'
'+e.map(function(t,e){var n="dp-month";return'"}).join("")+"
"}}};function d(o,r,a){var t,i,e,n,s,u,d=!1,c={el:void 0,opts:a,shouldFocusOnBlur:!0,shouldFocusOnRender:!0,state:{get selectedDate(){return i},set selectedDate(t){t&&!a.inRange(t)||(t?(i=new Date(t),c.state.hilightedDate=i):i=t,c.updateInput(i),r("select"),c.close())},view:"day"},adjustPosition:p,containerHTML:'
',attachToDom:function(){document.body.appendChild(c.el)},updateInput:function(t){var e=new y("change",{bubbles:!0});e.simulated=!0,o.value=t?a.format(t):"",o.dispatchEvent(e)},computeSelectedDate:function(){return a.parse(o.value)},currentView:function(){return b[c.state.view]},open:function(){var t,e,n;d||(c.el||(c.el=(t=a,e=c.containerHTML,(n=document.createElement("div")).className=t.mode,n.innerHTML=e,n),function(a){var t=a.el,e=t.querySelector(".dp");function n(n){n.target.className.split(" ").forEach(function(t){var e=a.currentView().onClick[t];e&&e(n,a)})}t.ontouchstart=p,D("blur",e,f(150,function(){a.hasFocus()||a.close(!0)})),D("keydown",t,function(t){t.keyCode===m.enter?n(t):a.currentView().onKeyDown(t,a)}),D("mousedown",e,function(t){t.target.focus&&t.target.focus(),document.activeElement!==t.target&&(t.preventDefault(),w(a))}),D("click",t,n)}(c)),i=l(c.computeSelectedDate(),a.min,a.max),c.state.hilightedDate=i||a.hilightedDate,c.state.view="day",c.attachToDom(),c.render(),r("open"))},isVisible:function(){return!!c.el&&!!c.el.parentNode},hasFocus:function(){var t=document.activeElement;return c.el&&c.el.contains(t)&&t.className.indexOf("dp-focuser")<0},shouldHide:function(){return c.isVisible()},close:function(t){var e=c.el;if(c.isVisible()){if(e){var n=e.parentNode;n&&n.removeChild(e)}var a;d=!0,t&&c.shouldFocusOnBlur&&((a=o).focus(),/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream&&a.blur()),setTimeout(function(){d=!1},100),r("close")}},destroy:function(){c.close(),t()},render:function(){if(c.el){var t=c.hasFocus(),e=c.currentView().render(c);e&&(c.el.firstChild.innerHTML=e),c.adjustPosition(),(t||c.shouldFocusOnRender)&&w(c)}},setState:function(t){for(var e in t)c.state[e]=t[e];r("statechange"),c.render()}};return e=o,n=c,s=f(5,function(){n.shouldHide()?n.close():n.open()}),u=[D("blur",e,f(150,function(){n.hasFocus()||n.close(!0)})),D("mousedown",e,function(){e===document.activeElement&&s()}),D("focus",e,s),D("input",e,function(t){var e=n.opts.parse(t.target.value);isNaN(e)||n.setState({hilightedDate:e})})],t=function(){u.forEach(function(t){t()})},c}function w(t){var e=t.el.querySelector(".dp-current");return e&&e.focus()}function c(S,t,e){var x=d(S,t,e);return x.shouldFocusOnBlur=!1,Object.defineProperty(x,"shouldFocusOnRender",{get:function(){return S!==document.activeElement}}),x.adjustPosition=function(){var t,e,n,a,o,r,i,s,u,d,c,l,f,p,h,v,g,m,D,y,b,w;c=x,l=S.getBoundingClientRect(),f=window,t=l,e=f,n=c.el,a=e.pageYOffset,o=a+t.top,r=n.offsetHeight,i=o+t.height+8,u=0<(s=o-r-8)&&i+r>a+e.innerHeight,d=u?s:i,n.classList&&(n.classList.toggle("dp-is-above",u),n.classList.toggle("dp-is-below",!u)),n.style.top=d+"px",p=l,h=f,v=c.el,g=h.pageXOffset,m=p.left+g,D=h.innerWidth+g,y=v.offsetWidth,b=D-y,w=D.',o):"dp-below"===n.mode?c(t,e,n):"dp-permanent"===n.mode?((s=d(r=t,e,i=n)).close=p,s.destroy=p,s.updateInput=p,s.shouldFocusOnRender=i.shouldFocusOnRender,s.computeSelectedDate=function(){return i.hilightedDate},s.attachToDom=function(){r.appendChild(s.el)},s.open(),s):void 0;var a,o,r,i,s}function x(){var a={};function n(t,e){(a[t]=a[t]||[]).push(e)}return{on:function(t,e){return e?n(t,e):function(t){for(var e in t)n(e,t[e])}(t),this},emit:function(e,n){(a[e]||[]).forEach(function(t){t(e,n)})},off:function(t,e){return t?a[t]=e?(a[t]||[]).filter(function(t){return t!==e}):[]:a={},this}}}function F(t,e){var n=x(),a=S(t,function(t){n.emit(t,o)},u(e)),o={get state(){return a.state},on:n.on,off:n.off,setState:a.setState,open:a.open,close:a.close,destroy:a.destroy};return o}var e=F;function C(t){return 12*t.getYear()+t.getMonth()}t.TinyDatePicker=e,t.DateRangePicker=function(t,e){e=e||{};var o,n=x(),a=(c=t,"string"==typeof c&&(c=document.querySelector(c)),c.innerHTML='
',c.querySelector(".dr-cals")),r={start:void 0,end:void 0},i=F(a.querySelector(".dr-cal-start"),g({},e.startOpts,{mode:"dp-permanent",dateClass:p})),s=F(a.querySelector(".dr-cal-end"),g({},e.endOpts,{mode:"dp-permanent",hilightedDate:v(i.state.hilightedDate,1),dateClass:p})),u={statechange:function(t,e){var n,a=i.state.hilightedDate,o=s.state.hilightedDate;1!=(n=a,C(o)-C(n))&&(e===i?s.setState({hilightedDate:v(e.state.hilightedDate,1)}):i.setState({hilightedDate:v(e.state.hilightedDate,-1)}))},select:function(t,e){var n=e.state.selectedDate;!r.start||r.end?l({start:n,end:void 0}):l({start:n>r.start?r.start:n,end:n>r.start?n:r.start})}},d={state:r,setState:l,on:n.on,off:n.off};var c;function l(t){for(var e in t)r[e]=t[e];n.emit("statechange",d),f()}function f(){i.setState({}),s.setState({})}function p(t){var e,n,a;return((r.end||o)&&r.start&&(e=t,n=r.end||o,a=r.start,e max) ? max : 133 | dt; 134 | } 135 | 136 | function dropTime(dt) { 137 | dt = new Date(dt); 138 | dt.setHours(0, 0, 0, 0); 139 | return dt; 140 | } 141 | 142 | /** 143 | * @file Utility functions for function manipulation. 144 | */ 145 | 146 | /** 147 | * bufferFn buffers calls to fn so they only happen every ms milliseconds 148 | * 149 | * @param {number} ms number of milliseconds 150 | * @param {function} fn the function to be buffered 151 | * @returns {function} 152 | */ 153 | function bufferFn(ms, fn) { 154 | var timeout = undefined; 155 | return function () { 156 | clearTimeout(timeout); 157 | timeout = setTimeout(fn, ms); 158 | }; 159 | } 160 | 161 | /** 162 | * noop is a function which does nothing at all. 163 | */ 164 | function noop() { } 165 | 166 | /** 167 | * copy properties from object o2 to object o1. 168 | * 169 | * @params {Object} o1 170 | * @params {Object} o2 171 | * @returns {Object} 172 | */ 173 | function cp() { 174 | var args = arguments; 175 | var o1 = args[0]; 176 | for (var i = 1; i < args.length; ++i) { 177 | var o2 = args[i] || {}; 178 | for (var key in o2) { 179 | o1[key] = o2[key]; 180 | } 181 | } 182 | return o1; 183 | } 184 | 185 | /** 186 | * @file Responsible for sanitizing and creating date picker options. 187 | */ 188 | 189 | var english = { 190 | days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 191 | months: [ 192 | 'January', 193 | 'February', 194 | 'March', 195 | 'April', 196 | 'May', 197 | 'June', 198 | 'July', 199 | 'August', 200 | 'September', 201 | 'October', 202 | 'November', 203 | 'December', 204 | ], 205 | today: 'Today', 206 | clear: 'Clear', 207 | close: 'Close', 208 | }; 209 | 210 | /** 211 | * DatePickerOptions constructs a new date picker options object, overriding 212 | * default values with any values specified in opts. 213 | * 214 | * @param {DatePickerOptions} opts 215 | * @returns {DatePickerOptions} 216 | */ 217 | function DatePickerOptions(opts) { 218 | opts = opts || {}; 219 | opts = cp(defaults(), opts); 220 | var parse = dateOrParse(opts.parse); 221 | opts.lang = cp(english, opts.lang); 222 | opts.parse = parse; 223 | opts.inRange = makeInRangeFn(opts); 224 | opts.min = parse(opts.min || shiftYear(now(), -100)); 225 | opts.max = parse(opts.max || shiftYear(now(), 100)); 226 | opts.hilightedDate = opts.parse(opts.hilightedDate); 227 | 228 | return opts; 229 | } 230 | 231 | function defaults() { 232 | return { 233 | lang: english, 234 | 235 | // Possible values: dp-modal, dp-below, dp-permanent 236 | mode: 'dp-modal', 237 | 238 | // The date to hilight initially if the date picker has no 239 | // initial value. 240 | hilightedDate: now(), 241 | 242 | format: function (dt) { 243 | return (dt.getMonth() + 1) + '/' + dt.getDate() + '/' + dt.getFullYear(); 244 | }, 245 | 246 | parse: function (str) { 247 | var date = new Date(str); 248 | return isNaN(date) ? now() : date; 249 | }, 250 | 251 | dateClass: function () { }, 252 | 253 | inRange: function () { 254 | return true; 255 | }, 256 | 257 | appendTo: document.body, 258 | }; 259 | } 260 | 261 | function makeInRangeFn(opts) { 262 | var inRange = opts.inRange; // Cache this version, and return a variant 263 | 264 | return function (dt, dp) { 265 | return inRange(dt, dp) && opts.min <= dt && opts.max >= dt; 266 | }; 267 | } 268 | 269 | /** 270 | * @file Helper functions for dealing with dom elements. 271 | */ 272 | 273 | var Key = { 274 | left: 37, 275 | up: 38, 276 | right: 39, 277 | down: 40, 278 | enter: 13, 279 | esc: 27, 280 | }; 281 | 282 | /** 283 | * on attaches an event handler to the specified element, and returns an 284 | * off function which can be used to remove the handler. 285 | * 286 | * @param {string} evt the name of the event to handle 287 | * @param {HTMLElement} el the element to attach to 288 | * @param {function} handler the event handler 289 | * @returns {function} the off function 290 | */ 291 | function on(evt, el, handler) { 292 | el.addEventListener(evt, handler, true); 293 | 294 | return function () { 295 | el.removeEventListener(evt, handler, true); 296 | }; 297 | } 298 | 299 | var CustomEvent = shimCustomEvent(); 300 | 301 | function shimCustomEvent() { 302 | var CustomEvent = window.CustomEvent; 303 | 304 | if (typeof CustomEvent !== 'function') { 305 | CustomEvent = function (event, params) { 306 | params = params || {bubbles: false, cancelable: false, detail: undefined}; 307 | var evt = document.createEvent('CustomEvent'); 308 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 309 | return evt; 310 | }; 311 | 312 | CustomEvent.prototype = window.Event.prototype; 313 | } 314 | 315 | return CustomEvent; 316 | } 317 | 318 | /** 319 | * @file Manages the calendar / day-picker view. 320 | */ 321 | 322 | var dayPicker = { 323 | onKeyDown: keyDown, 324 | onClick: { 325 | 'dp-day': selectDay, 326 | 'dp-next': gotoNextMonth, 327 | 'dp-prev': gotoPrevMonth, 328 | 'dp-today': selectToday, 329 | 'dp-clear': clear, 330 | 'dp-close': close, 331 | 'dp-cal-month': showMonthPicker, 332 | 'dp-cal-year': showYearPicker, 333 | }, 334 | render: render 335 | }; 336 | 337 | /** 338 | * view renders the calendar (day picker) as an HTML string. 339 | * 340 | * @param {DatePickerContext} context the date picker being rendered 341 | * @returns {string} 342 | */ 343 | function render(dp) { 344 | var opts = dp.opts; 345 | var lang = opts.lang; 346 | var state = dp.state; 347 | var dayNames = lang.days; 348 | var dayOffset = opts.dayOffset || 0; 349 | var selectedDate = state.selectedDate; 350 | var hilightedDate = state.hilightedDate; 351 | var hilightedMonth = hilightedDate.getMonth(); 352 | var today = now().getTime(); 353 | 354 | return ( 355 | '
' + 356 | '
' + 357 | '' + 358 | '' + 361 | '' + 364 | '' + 365 | '
' + 366 | '
' + 367 | dayNames.map(function (name, i) { 368 | return ( 369 | '' + dayNames[(i + dayOffset) % dayNames.length] + '' 370 | ); 371 | }).join('') + 372 | mapDays(hilightedDate, dayOffset, function (date) { 373 | var isNotInMonth = date.getMonth() !== hilightedMonth; 374 | var isDisabled = !opts.inRange(date); 375 | var isToday = date.getTime() === today; 376 | var className = 'dp-day'; 377 | className += (isNotInMonth ? ' dp-edge-day' : ''); 378 | className += (datesEq(date, hilightedDate) ? ' dp-current' : ''); 379 | className += (datesEq(date, selectedDate) ? ' dp-selected' : ''); 380 | className += (isDisabled ? ' dp-day-disabled' : ''); 381 | className += (isToday ? ' dp-day-today' : ''); 382 | className += ' ' + opts.dateClass(date, dp); 383 | 384 | return ( 385 | '' 388 | ); 389 | }) + 390 | '
' + 391 | '
' + 392 | '' + 393 | '' + 394 | '' + 395 | '
' + 396 | '
' 397 | ); 398 | } 399 | 400 | /** 401 | * keyDown handles the key down event for the day-picker 402 | * 403 | * @param {Event} e 404 | * @param {DatePickerContext} dp 405 | */ 406 | function keyDown(e, dp) { 407 | var key = e.keyCode; 408 | var shiftBy = 409 | (key === Key.left) ? -1 : 410 | (key === Key.right) ? 1 : 411 | (key === Key.up) ? -7 : 412 | (key === Key.down) ? 7 : 413 | 0; 414 | 415 | if (key === Key.esc) { 416 | dp.close(); 417 | } else if (shiftBy) { 418 | e.preventDefault(); 419 | dp.setState({ 420 | hilightedDate: shiftDay(dp.state.hilightedDate, shiftBy) 421 | }); 422 | } 423 | } 424 | 425 | function selectToday(e, dp) { 426 | dp.setState({ 427 | selectedDate: now(), 428 | }); 429 | } 430 | 431 | function clear(e, dp) { 432 | dp.setState({ 433 | selectedDate: null, 434 | }); 435 | } 436 | 437 | function close(e, dp) { 438 | dp.close(); 439 | } 440 | 441 | function showMonthPicker(e, dp) { 442 | dp.setState({ 443 | view: 'month' 444 | }); 445 | } 446 | 447 | function showYearPicker(e, dp) { 448 | dp.setState({ 449 | view: 'year' 450 | }); 451 | } 452 | 453 | function gotoNextMonth(e, dp) { 454 | var hilightedDate = dp.state.hilightedDate; 455 | dp.setState({ 456 | hilightedDate: shiftMonth(hilightedDate, 1) 457 | }); 458 | } 459 | 460 | function gotoPrevMonth(e, dp) { 461 | var hilightedDate = dp.state.hilightedDate; 462 | dp.setState({ 463 | hilightedDate: shiftMonth(hilightedDate, -1) 464 | }); 465 | } 466 | 467 | function selectDay(e, dp) { 468 | dp.setState({ 469 | selectedDate: new Date(parseInt(e.target.getAttribute('data-date'))), 470 | }); 471 | } 472 | 473 | function mapDays(currentDate, dayOffset, fn) { 474 | var result = ''; 475 | var iter = new Date(currentDate); 476 | iter.setDate(1); 477 | iter.setDate(1 - iter.getDay() + dayOffset); 478 | 479 | // If we are showing monday as the 1st of the week, 480 | // and the monday is the 2nd of the month, the sunday won't 481 | // show, so we need to shift backwards 482 | if (dayOffset && iter.getDate() === dayOffset + 1) { 483 | iter.setDate(dayOffset - 6); 484 | } 485 | 486 | // We are going to have 6 weeks always displayed to keep a consistent 487 | // calendar size 488 | for (var day = 0; day < (6 * 7); ++day) { 489 | result += fn(iter); 490 | iter.setDate(iter.getDate() + 1); 491 | } 492 | 493 | return result; 494 | } 495 | 496 | /** 497 | * @file Manages the month-picker view. 498 | */ 499 | 500 | var monthPicker = { 501 | onKeyDown: keyDown$1, 502 | onClick: { 503 | 'dp-month': onChooseMonth 504 | }, 505 | render: render$1 506 | }; 507 | 508 | function onChooseMonth(e, dp) { 509 | dp.setState({ 510 | hilightedDate: setMonth(dp.state.hilightedDate, parseInt(e.target.getAttribute('data-month'))), 511 | view: 'day', 512 | }); 513 | } 514 | 515 | /** 516 | * render renders the month picker as an HTML string 517 | * 518 | * @param {DatePickerContext} dp the date picker context 519 | * @returns {string} 520 | */ 521 | function render$1(dp) { 522 | var opts = dp.opts; 523 | var lang = opts.lang; 524 | var months = lang.months; 525 | var currentDate = dp.state.hilightedDate; 526 | var currentMonth = currentDate.getMonth(); 527 | 528 | return ( 529 | '
' + 530 | months.map(function (month, i) { 531 | var className = 'dp-month'; 532 | className += (currentMonth === i ? ' dp-current' : ''); 533 | 534 | return ( 535 | '' 538 | ); 539 | }).join('') + 540 | '
' 541 | ); 542 | } 543 | 544 | /** 545 | * keyDown handles keydown events that occur in the month picker 546 | * 547 | * @param {Event} e 548 | * @param {DatePickerContext} dp 549 | */ 550 | function keyDown$1(e, dp) { 551 | var key = e.keyCode; 552 | var shiftBy = 553 | (key === Key.left) ? -1 : 554 | (key === Key.right) ? 1 : 555 | (key === Key.up) ? -3 : 556 | (key === Key.down) ? 3 : 557 | 0; 558 | 559 | if (key === Key.esc) { 560 | dp.setState({ 561 | view: 'day', 562 | }); 563 | } else if (shiftBy) { 564 | e.preventDefault(); 565 | dp.setState({ 566 | hilightedDate: shiftMonth(dp.state.hilightedDate, shiftBy, true) 567 | }); 568 | } 569 | } 570 | 571 | /** 572 | * @file Manages the year-picker view. 573 | */ 574 | 575 | var yearPicker = { 576 | render: render$2, 577 | onKeyDown: keyDown$2, 578 | onClick: { 579 | 'dp-year': onChooseYear 580 | }, 581 | }; 582 | 583 | /** 584 | * view renders the year picker as an HTML string. 585 | * 586 | * @param {DatePickerContext} dp the date picker context 587 | * @returns {string} 588 | */ 589 | function render$2(dp) { 590 | var state = dp.state; 591 | var currentYear = state.hilightedDate.getFullYear(); 592 | var selectedYear = state.selectedDate.getFullYear(); 593 | 594 | return ( 595 | '
' + 596 | mapYears(dp, function (year) { 597 | var className = 'dp-year'; 598 | className += (year === currentYear ? ' dp-current' : ''); 599 | className += (year === selectedYear ? ' dp-selected' : ''); 600 | 601 | return ( 602 | '' 605 | ); 606 | }) + 607 | '
' 608 | ); 609 | } 610 | 611 | function onChooseYear(e, dp) { 612 | dp.setState({ 613 | hilightedDate: setYear(dp.state.hilightedDate, parseInt(e.target.getAttribute('data-year'))), 614 | view: 'day', 615 | }); 616 | } 617 | 618 | function keyDown$2(e, dp) { 619 | var key = e.keyCode; 620 | var opts = dp.opts; 621 | var shiftBy = 622 | (key === Key.left || key === Key.up) ? 1 : 623 | (key === Key.right || key === Key.down) ? -1 : 624 | 0; 625 | 626 | if (key === Key.esc) { 627 | dp.setState({ 628 | view: 'day', 629 | }); 630 | } else if (shiftBy) { 631 | e.preventDefault(); 632 | var shiftedYear = shiftYear(dp.state.hilightedDate, shiftBy); 633 | 634 | dp.setState({ 635 | hilightedDate: constrainDate(shiftedYear, opts.min, opts.max), 636 | }); 637 | } 638 | } 639 | 640 | function mapYears(dp, fn) { 641 | var result = ''; 642 | var max = dp.opts.max.getFullYear(); 643 | 644 | for (var i = max; i >= dp.opts.min.getFullYear(); --i) { 645 | result += fn(i); 646 | } 647 | 648 | return result; 649 | } 650 | 651 | /** 652 | * @file Defines the base date picker behavior, overridden by various modes. 653 | */ 654 | 655 | var views = { 656 | day: dayPicker, 657 | year: yearPicker, 658 | month: monthPicker 659 | }; 660 | 661 | function BaseMode(input, emit, opts) { 662 | var detatchInputEvents; // A function that detaches all events from the input 663 | var closing = false; // A hack to prevent calendar from re-opening when closing. 664 | var selectedDate; // The currently selected date 665 | var dp = { 666 | // The root DOM element for the date picker, initialized on first open. 667 | el: undefined, 668 | opts: opts, 669 | shouldFocusOnBlur: true, 670 | shouldFocusOnRender: true, 671 | state: initialState(), 672 | adjustPosition: noop, 673 | containerHTML: '
', 674 | 675 | attachToDom: function () { 676 | var appendTo = opts.appendTo || document.body; 677 | appendTo.appendChild(dp.el); 678 | }, 679 | 680 | updateInput: function (selectedDate) { 681 | var e = new CustomEvent('change', {bubbles: true}); 682 | e.simulated = true; 683 | input.value = selectedDate ? opts.format(selectedDate) : ''; 684 | input.dispatchEvent(e); 685 | }, 686 | 687 | computeSelectedDate: function () { 688 | return opts.parse(input.value); 689 | }, 690 | 691 | currentView: function() { 692 | return views[dp.state.view]; 693 | }, 694 | 695 | open: function () { 696 | if (closing) { 697 | return; 698 | } 699 | 700 | if (!dp.el) { 701 | dp.el = createContainerElement(opts, dp.containerHTML); 702 | attachContainerEvents(dp); 703 | } 704 | 705 | selectedDate = constrainDate(dp.computeSelectedDate(), opts.min, opts.max); 706 | dp.state.hilightedDate = selectedDate || opts.hilightedDate; 707 | dp.state.view = 'day'; 708 | 709 | dp.attachToDom(); 710 | dp.render(); 711 | 712 | emit('open'); 713 | }, 714 | 715 | isVisible: function () { 716 | return !!dp.el && !!dp.el.parentNode; 717 | }, 718 | 719 | hasFocus: function () { 720 | var focused = document.activeElement; 721 | return dp.el && 722 | dp.el.contains(focused) && 723 | focused.className.indexOf('dp-focuser') < 0; 724 | }, 725 | 726 | shouldHide: function () { 727 | return dp.isVisible(); 728 | }, 729 | 730 | close: function (becauseOfBlur) { 731 | var el = dp.el; 732 | 733 | if (!dp.isVisible()) { 734 | return; 735 | } 736 | 737 | if (el) { 738 | var parent = el.parentNode; 739 | parent && parent.removeChild(el); 740 | } 741 | 742 | closing = true; 743 | 744 | if (becauseOfBlur && dp.shouldFocusOnBlur) { 745 | focusInput(input); 746 | } 747 | 748 | // When we close, the input often gains refocus, which 749 | // can then launch the date picker again, so we buffer 750 | // a bit and don't show the date picker within N ms of closing 751 | setTimeout(function() { 752 | closing = false; 753 | }, 100); 754 | 755 | emit('close'); 756 | }, 757 | 758 | destroy: function () { 759 | dp.close(); 760 | detatchInputEvents(); 761 | }, 762 | 763 | render: function () { 764 | if (!dp.el) { 765 | return; 766 | } 767 | 768 | var hadFocus = dp.hasFocus(); 769 | var html = dp.currentView().render(dp); 770 | html && (dp.el.firstChild.innerHTML = html); 771 | 772 | dp.adjustPosition(); 773 | 774 | if (hadFocus || dp.shouldFocusOnRender) { 775 | focusCurrent(dp); 776 | } 777 | }, 778 | 779 | // Conceptually similar to setState in React, updates 780 | // the view state and re-renders. 781 | setState: function (state) { 782 | for (var key in state) { 783 | dp.state[key] = state[key]; 784 | } 785 | 786 | emit('statechange'); 787 | dp.render(); 788 | }, 789 | }; 790 | 791 | detatchInputEvents = attachInputEvents(input, dp); 792 | 793 | // Builds the initial view state 794 | // selectedDate is a special case and causes changes to hilightedDate 795 | // hilightedDate is set on open, so remains undefined initially 796 | // view is the current view (day, month, year) 797 | function initialState() { 798 | return { 799 | get selectedDate() { 800 | return selectedDate; 801 | }, 802 | set selectedDate(dt) { 803 | if (dt && !opts.inRange(dt)) { 804 | return; 805 | } 806 | 807 | if (dt) { 808 | selectedDate = new Date(dt); 809 | dp.state.hilightedDate = selectedDate; 810 | } else { 811 | selectedDate = dt; 812 | } 813 | 814 | dp.updateInput(selectedDate); 815 | emit('select'); 816 | dp.close(); 817 | }, 818 | view: 'day', 819 | }; 820 | } 821 | 822 | return dp; 823 | } 824 | 825 | function createContainerElement(opts, containerHTML) { 826 | var el = document.createElement('div'); 827 | 828 | el.className = opts.mode; 829 | el.innerHTML = containerHTML; 830 | 831 | return el; 832 | } 833 | 834 | function attachInputEvents(input, dp) { 835 | var bufferShow = bufferFn(5, function () { 836 | if (dp.shouldHide()) { 837 | dp.close(); 838 | } else { 839 | dp.open(); 840 | } 841 | }); 842 | 843 | var off = [ 844 | on('blur', input, bufferFn(150, function () { 845 | if (!dp.hasFocus()) { 846 | dp.close(true); 847 | } 848 | })), 849 | 850 | on('mousedown', input, function () { 851 | if (input === document.activeElement) { 852 | bufferShow(); 853 | } 854 | }), 855 | 856 | on('focus', input, bufferShow), 857 | 858 | on('input', input, function (e) { 859 | var date = dp.opts.parse(e.target.value); 860 | isNaN(date) || dp.setState({ 861 | hilightedDate: date 862 | }); 863 | }), 864 | ]; 865 | 866 | // Unregister all events that were registered above. 867 | return function() { 868 | off.forEach(function (f) { 869 | f(); 870 | }); 871 | }; 872 | } 873 | 874 | function focusCurrent(dp) { 875 | var current = dp.el.querySelector('.dp-current'); 876 | return current && current.focus(); 877 | } 878 | 879 | function attachContainerEvents(dp) { 880 | var el = dp.el; 881 | var calEl = el.querySelector('.dp'); 882 | 883 | // Hack to get iOS to show active CSS states 884 | el.ontouchstart = noop; 885 | 886 | function onClick(e) { 887 | e.target.className.split(' ').forEach(function(evt) { 888 | var handler = dp.currentView().onClick[evt]; 889 | handler && handler(e, dp); 890 | }); 891 | } 892 | 893 | // The calender fires a blur event *every* time we redraw 894 | // this means we need to buffer the blur event to see if 895 | // it still has no focus after redrawing, and only then 896 | // do we return focus to the input. A possible other approach 897 | // would be to set context.redrawing = true on redraw and 898 | // set it to false in the blur event. 899 | on('blur', calEl, bufferFn(150, function () { 900 | if (!dp.hasFocus()) { 901 | dp.close(true); 902 | } 903 | })); 904 | 905 | on('keydown', el, function (e) { 906 | if (e.keyCode === Key.enter) { 907 | onClick(e); 908 | } else { 909 | dp.currentView().onKeyDown(e, dp); 910 | } 911 | }); 912 | 913 | // If the user clicks in non-focusable space, but 914 | // still within the date picker, we don't want to 915 | // hide, so we need to hack some things... 916 | on('mousedown', calEl, function (e) { 917 | e.target.focus && e.target.focus(); // IE hack 918 | if (document.activeElement !== e.target) { 919 | e.preventDefault(); 920 | focusCurrent(dp); 921 | } 922 | }); 923 | 924 | on('click', el, onClick); 925 | } 926 | 927 | function focusInput(input) { 928 | // When the modal closes, we need to focus the original input so the 929 | // user can continue tabbing from where they left off. 930 | input.focus(); 931 | 932 | // iOS zonks out if we don't blur the input, so... 933 | if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) { 934 | input.blur(); 935 | } 936 | } 937 | 938 | /** 939 | * @file Defines the modal date picker behavior. 940 | */ 941 | 942 | function ModalMode(input, emit, opts) { 943 | var dp = BaseMode(input, emit, opts); 944 | 945 | // In modal mode, users really shouldn't be able to type in 946 | // the input, as all input is done via the calendar. 947 | input.readonly = true; 948 | 949 | // In modal mode, we need to know when the user has tabbed 950 | // off the end of the calendar, and set focus to the original 951 | // input. To do this, we add a special element to the DOM. 952 | // When the user tabs off the bottom of the calendar, they 953 | // will tab onto this element. 954 | dp.containerHTML += '.'; 955 | 956 | return dp; 957 | } 958 | 959 | /** 960 | * @file Defines the dropdown date picker behavior. 961 | */ 962 | 963 | function DropdownMode(input, emit, opts) { 964 | var dp = BaseMode(input, emit, opts); 965 | 966 | dp.shouldFocusOnBlur = false; 967 | 968 | Object.defineProperty(dp, 'shouldFocusOnRender', { 969 | get: function() { 970 | return input !== document.activeElement; 971 | } 972 | }); 973 | 974 | dp.adjustPosition = function () { 975 | autoPosition(input, dp); 976 | }; 977 | 978 | return dp; 979 | } 980 | 981 | function autoPosition(input, dp) { 982 | var inputPos = input.getBoundingClientRect(); 983 | var win = window; 984 | 985 | adjustCalY(dp, inputPos, win); 986 | adjustCalX(dp, inputPos, win); 987 | 988 | dp.el.style.visibility = ''; 989 | } 990 | 991 | function adjustCalX(dp, inputPos, win) { 992 | var cal = dp.el; 993 | var scrollLeft = win.pageXOffset; 994 | var inputLeft = inputPos.left + scrollLeft; 995 | var maxRight = win.innerWidth + scrollLeft; 996 | var offsetWidth = cal.offsetWidth; 997 | var calRight = inputLeft + offsetWidth; 998 | var shiftedLeft = maxRight - offsetWidth; 999 | var left = calRight > maxRight && shiftedLeft > 0 ? shiftedLeft : inputLeft; 1000 | 1001 | cal.style.left = left + 'px'; 1002 | } 1003 | 1004 | function adjustCalY(dp, inputPos, win) { 1005 | var cal = dp.el; 1006 | var scrollTop = win.pageYOffset; 1007 | var inputTop = scrollTop + inputPos.top; 1008 | var calHeight = cal.offsetHeight; 1009 | var belowTop = inputTop + inputPos.height + 8; 1010 | var aboveTop = inputTop - calHeight - 8; 1011 | var isAbove = (aboveTop > 0 && belowTop + calHeight > scrollTop + win.innerHeight); 1012 | var top = isAbove ? aboveTop : belowTop; 1013 | 1014 | if (cal.classList) { 1015 | cal.classList.toggle('dp-is-above', isAbove); 1016 | cal.classList.toggle('dp-is-below', !isAbove); 1017 | } 1018 | cal.style.top = top + 'px'; 1019 | } 1020 | 1021 | /** 1022 | * @file Defines the permanent date picker behavior. 1023 | */ 1024 | 1025 | function PermanentMode(root, emit, opts) { 1026 | var dp = BaseMode(root, emit, opts); 1027 | 1028 | dp.close = noop; 1029 | dp.updateInput = noop; 1030 | dp.shouldFocusOnRender = opts.shouldFocusOnRender; 1031 | 1032 | dp.computeSelectedDate = function () { 1033 | return opts.hilightedDate; 1034 | }; 1035 | 1036 | dp.attachToDom = function () { 1037 | var appendTo = opts.appendTo || root; 1038 | appendTo.appendChild(dp.el); 1039 | }; 1040 | 1041 | dp.open(); 1042 | 1043 | return dp; 1044 | } 1045 | 1046 | /** 1047 | * @file Defines the various date picker modes (modal, dropdown, permanent) 1048 | */ 1049 | 1050 | function Mode(input, emit, opts) { 1051 | input = input && input.tagName ? input : document.querySelector(input); 1052 | 1053 | if (opts.mode === 'dp-modal') { 1054 | return ModalMode(input, emit, opts); 1055 | } 1056 | 1057 | if (opts.mode === 'dp-below') { 1058 | return DropdownMode(input, emit, opts); 1059 | } 1060 | 1061 | if (opts.mode === 'dp-permanent') { 1062 | return PermanentMode(input, emit, opts); 1063 | } 1064 | } 1065 | 1066 | /** 1067 | * @file Defines simple event emitter behavior. 1068 | */ 1069 | 1070 | /** 1071 | * Emitter constructs a new emitter object which has on/off methods. 1072 | * 1073 | * @returns {EventEmitter} 1074 | */ 1075 | function Emitter() { 1076 | var handlers = {}; 1077 | 1078 | function onOne(name, handler) { 1079 | (handlers[name] = (handlers[name] || [])).push(handler); 1080 | } 1081 | 1082 | function onMany(fns) { 1083 | for (var name in fns) { 1084 | onOne(name, fns[name]); 1085 | } 1086 | } 1087 | 1088 | return { 1089 | on: function (name, handler) { 1090 | if (handler) { 1091 | onOne(name, handler); 1092 | } else { 1093 | onMany(name); 1094 | } 1095 | 1096 | return this; 1097 | }, 1098 | 1099 | emit: function (name, arg) { 1100 | (handlers[name] || []).forEach(function (handler) { 1101 | handler(name, arg); 1102 | }); 1103 | }, 1104 | 1105 | off: function (name, handler) { 1106 | if (!name) { 1107 | handlers = {}; 1108 | } else if (!handler) { 1109 | handlers[name] = []; 1110 | } else { 1111 | handlers[name] = (handlers[name] || []).filter(function (h) { 1112 | return h !== handler; 1113 | }); 1114 | } 1115 | 1116 | return this; 1117 | } 1118 | }; 1119 | } 1120 | 1121 | /** 1122 | * @file The root date picker file, defines public exports for the library. 1123 | */ 1124 | 1125 | /** 1126 | * The date picker language configuration 1127 | * @typedef {Object} LangOptions 1128 | * @property {Array.} [days] - Days of the week 1129 | * @property {Array.} [months] - Months of the year 1130 | * @property {string} today - The label for the 'today' button 1131 | * @property {string} close - The label for the 'close' button 1132 | * @property {string} clear - The label for the 'clear' button 1133 | */ 1134 | 1135 | /** 1136 | * The configuration options for a date picker. 1137 | * 1138 | * @typedef {Object} DatePickerOptions 1139 | * @property {LangOptions} [lang] - Configures the label text, defaults to English 1140 | * @property {('dp-modal'|'dp-below'|'dp-permanent')} [mode] - The date picker mode, defaults to 'dp-modal' 1141 | * @property {(string|Date)} [hilightedDate] - The date to hilight if no date is selected 1142 | * @property {function(string|Date):Date} [parse] - Parses a date, the complement of the "format" function 1143 | * @property {function(Date):string} [format] - Formats a date for displaying to user 1144 | * @property {function(Date):string} [dateClass] - Associates a custom CSS class with a date 1145 | * @property {function(Date):boolean} [inRange] - Indicates whether or not a date is selectable 1146 | * @property {(string|Date)} [min] - The minimum selectable date (inclusive, default 100 years ago) 1147 | * @property {(string|Date)} [max] - The maximum selectable date (inclusive, default 100 years from now) 1148 | */ 1149 | 1150 | /** 1151 | * The state values for the date picker 1152 | * 1153 | * @typedef {Object} DatePickerState 1154 | * @property {string} view - The current view 'day' | 'month' | 'year' 1155 | * @property {Date} selectedDate - The date which has been selected by the user 1156 | * @property {Date} hilightedDate - The date which is currently hilighted / active 1157 | */ 1158 | 1159 | /** 1160 | * An instance of TinyDatePicker 1161 | * 1162 | * @typedef {Object} DatePicker 1163 | * @property {DatePickerState} state - The values currently displayed. 1164 | * @property {function} on - Adds an event handler 1165 | * @property {function} off - Removes an event handler 1166 | * @property {function} setState - Changes the current state of the date picker 1167 | * @property {function} open - Opens the date picker 1168 | * @property {function} close - Closes the date picker 1169 | * @property {function} destroy - Destroys the date picker (removing all handlers from the input, too) 1170 | */ 1171 | 1172 | /** 1173 | * TinyDatePicker constructs a new date picker for the specified input 1174 | * 1175 | * @param {HTMLElement | string} input The input or CSS selector associated with the datepicker 1176 | * @param {DatePickerOptions} opts The options for initializing the date picker 1177 | * @returns {DatePicker} 1178 | */ 1179 | function TinyDatePicker(input, opts) { 1180 | var emitter = Emitter(); 1181 | var options = DatePickerOptions(opts); 1182 | var mode = Mode(input, emit, options); 1183 | var me = { 1184 | get state() { 1185 | return mode.state; 1186 | }, 1187 | on: emitter.on, 1188 | off: emitter.off, 1189 | setState: mode.setState, 1190 | open: mode.open, 1191 | close: mode.close, 1192 | destroy: mode.destroy, 1193 | }; 1194 | 1195 | function emit(evt) { 1196 | emitter.emit(evt, me); 1197 | } 1198 | 1199 | return me; 1200 | } 1201 | 1202 | return TinyDatePicker; 1203 | 1204 | }))); 1205 | -------------------------------------------------------------------------------- /dist/tiny-date-picker.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.TinyDatePicker=e()}(this,function(){"use strict";function o(){var t=new Date;return t.setHours(0,0,0,0),t}function l(t,e){return(t&&t.toDateString())===(e&&e.toDateString())}function r(t,e,n){var a=(t=new Date(t)).getDate(),o=t.getMonth()+e;return t.setDate(1),t.setMonth(n?(12+o)%12:o),t.setDate(a),t.getDate()=t}),t.min=e(t.min||i(o(),-100)),t.max=e(t.max||i(o(),100)),t.hilightedDate=t.parse(t.hilightedDate),t}var v={left:37,up:38,right:39,down:40,enter:13,esc:27};function g(t,e,n){return e.addEventListener(t,n,!0),function(){e.removeEventListener(t,n,!0)}}var m=function(){var t=window.CustomEvent;"function"!=typeof t&&((t=function(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n}).prototype=window.Event.prototype);return t}();var y={day:{onKeyDown:function(t,e){var n=t.keyCode,a=n===v.left?-1:n===v.right?1:n===v.up?-7:n===v.down?7:0;n===v.esc?e.close():a&&(t.preventDefault(),e.setState({hilightedDate:(o=e.state.hilightedDate,r=a,(o=new Date(o)).setDate(o.getDate()+r),o)}));var o,r},onClick:{"dp-day":function(t,e){e.setState({selectedDate:new Date(parseInt(t.target.getAttribute("data-date")))})},"dp-next":function(t,e){var n=e.state.hilightedDate;e.setState({hilightedDate:r(n,1)})},"dp-prev":function(t,e){var n=e.state.hilightedDate;e.setState({hilightedDate:r(n,-1)})},"dp-today":function(t,e){e.setState({selectedDate:o()})},"dp-clear":function(t,e){e.setState({selectedDate:null})},"dp-close":function(t,e){e.close()},"dp-cal-month":function(t,e){e.setState({view:"month"})},"dp-cal-year":function(t,e){e.setState({view:"year"})}},render:function(r){var i=r.opts,t=i.lang,e=r.state,n=t.days,a=i.dayOffset||0,u=e.selectedDate,s=e.hilightedDate,d=s.getMonth(),c=o().getTime();return'
'+n.map(function(t,e){return''+n[(e+a)%n.length]+""}).join("")+function(t,e,n){var a="",o=new Date(t);o.setDate(1),o.setDate(1-o.getDay()+e),e&&o.getDate()===e+1&&o.setDate(e-6);for(var r=0;r<42;++r)a+=n(o),o.setDate(o.getDate()+1);return a}(s,a,function(t){var e=t.getMonth()!==d,n=!i.inRange(t),a=t.getTime()===c,o="dp-day";return o+=e?" dp-edge-day":"",o+=l(t,s)?" dp-current":"",o+=l(t,u)?" dp-selected":"",o+=n?" dp-day-disabled":"",o+=a?" dp-day-today":"",'"})+'
"}},year:{render:function(t){var e=t.state,n=e.hilightedDate.getFullYear(),a=e.selectedDate.getFullYear();return'
'+function(t,e){for(var n="",a=t.opts.max.getFullYear();a>=t.opts.min.getFullYear();--a)n+=e(a);return n}(t,function(t){var e="dp-year";return e+=t===n?" dp-current":"",'"})+"
"},onKeyDown:function(t,e){var n=t.keyCode,a=e.opts,o=n===v.left||n===v.up?1:n===v.right||n===v.down?-1:0;if(n===v.esc)e.setState({view:"day"});else if(o){t.preventDefault();var r=i(e.state.hilightedDate,o);e.setState({hilightedDate:f(r,a.min,a.max)})}},onClick:{"dp-year":function(t,e){e.setState({hilightedDate:(n=e.state.hilightedDate,a=parseInt(t.target.getAttribute("data-year")),(n=new Date(n)).setFullYear(a),n),view:"day"});var n,a}}},month:{onKeyDown:function(t,e){var n=t.keyCode,a=n===v.left?-1:n===v.right?1:n===v.up?-3:n===v.down?3:0;n===v.esc?e.setState({view:"day"}):a&&(t.preventDefault(),e.setState({hilightedDate:r(e.state.hilightedDate,a,!0)}))},onClick:{"dp-month":function(t,e){e.setState({hilightedDate:(n=e.state.hilightedDate,a=parseInt(t.target.getAttribute("data-month")),r(n,a-n.getMonth())),view:"day"});var n,a}},render:function(t){var e=t.opts.lang.months,a=t.state.hilightedDate.getMonth();return'
'+e.map(function(t,e){var n="dp-month";return'"}).join("")+"
"}}};function D(o,r,a){var t,i,e,n,u,s,d=!1,c={el:void 0,opts:a,shouldFocusOnBlur:!0,shouldFocusOnRender:!0,state:{get selectedDate(){return i},set selectedDate(t){t&&!a.inRange(t)||(t?(i=new Date(t),c.state.hilightedDate=i):i=t,c.updateInput(i),r("select"),c.close())},view:"day"},adjustPosition:h,containerHTML:'
',attachToDom:function(){document.body.appendChild(c.el)},updateInput:function(t){var e=new m("change",{bubbles:!0});e.simulated=!0,o.value=t?a.format(t):"",o.dispatchEvent(e)},computeSelectedDate:function(){return a.parse(o.value)},currentView:function(){return y[c.state.view]},open:function(){var t,e,n;d||(c.el||(c.el=(t=a,e=c.containerHTML,(n=document.createElement("div")).className=t.mode,n.innerHTML=e,n),function(a){var t=a.el,e=t.querySelector(".dp");function n(n){n.target.className.split(" ").forEach(function(t){var e=a.currentView().onClick[t];e&&e(n,a)})}t.ontouchstart=h,g("blur",e,p(150,function(){a.hasFocus()||a.close(!0)})),g("keydown",t,function(t){t.keyCode===v.enter?n(t):a.currentView().onKeyDown(t,a)}),g("mousedown",e,function(t){t.target.focus&&t.target.focus(),document.activeElement!==t.target&&(t.preventDefault(),b(a))}),g("click",t,n)}(c)),i=f(c.computeSelectedDate(),a.min,a.max),c.state.hilightedDate=i||a.hilightedDate,c.state.view="day",c.attachToDom(),c.render(),r("open"))},isVisible:function(){return!!c.el&&!!c.el.parentNode},hasFocus:function(){var t=document.activeElement;return c.el&&c.el.contains(t)&&t.className.indexOf("dp-focuser")<0},shouldHide:function(){return c.isVisible()},close:function(t){var e=c.el;if(c.isVisible()){if(e){var n=e.parentNode;n&&n.removeChild(e)}var a;d=!0,t&&c.shouldFocusOnBlur&&((a=o).focus(),/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream&&a.blur()),setTimeout(function(){d=!1},100),r("close")}},destroy:function(){c.close(),t()},render:function(){if(c.el&&c.el.firstChild){var t=c.hasFocus(),e=c.currentView().render(c);e&&(c.el.firstChild.innerHTML=e),c.adjustPosition(),(t||c.shouldFocusOnRender)&&b(c)}},setState:function(t){for(var e in t)c.state[e]=t[e];r("statechange"),c.render()}};return e=o,n=c,u=p(5,function(){n.shouldHide()?n.close():n.open()}),s=[g("blur",e,p(150,function(){n.hasFocus()||n.close(!0)})),g("mousedown",e,function(){e===document.activeElement&&u()}),g("focus",e,u),g("input",e,function(t){var e=n.opts.parse(t.target.value);isNaN(e)||n.setState({hilightedDate:e})})],t=function(){s.forEach(function(t){t()})},c}function b(t){var e=t.el.querySelector(".dp-current");return e&&e.focus()}function w(S,t,e){var x=D(S,t,e);return x.shouldFocusOnBlur=!1,Object.defineProperty(x,"shouldFocusOnRender",{get:function(){return S!==document.activeElement}}),x.adjustPosition=function(){var t,e,n,a,o,r,i,u,s,d,c,l,f,p,h,v,g,m,y,D,b,w;c=x,l=S.getBoundingClientRect(),f=window,t=l,e=f,n=c.el,a=e.pageYOffset,o=a+t.top,r=n.offsetHeight,i=o+t.height+8,s=0<(u=o-r-8)&&i+r>a+e.innerHeight,d=s?u:i,n.classList&&(n.classList.toggle("dp-is-above",s),n.classList.toggle("dp-is-below",!s)),n.style.top=d+"px",p=l,h=f,v=c.el,g=h.pageXOffset,m=p.left+g,y=h.innerWidth+g,D=v.offsetWidth,b=y-D,w=y.',o):"dp-below"===n.mode?w(t,e,n):"dp-permanent"===n.mode?((u=D(r=t,e,i=n)).close=h,u.destroy=h,u.updateInput=h,u.shouldFocusOnRender=i.shouldFocusOnRender,u.computeSelectedDate=function(){return i.hilightedDate},u.attachToDom=function(){r.appendChild(u.el)},u.open(),u):void 0;var a,o,r,i,u}function x(){var a={};function n(t,e){(a[t]=a[t]||[]).push(e)}return{on:function(t,e){return e?n(t,e):function(t){for(var e in t)n(e,t[e])}(t),this},emit:function(e,n){(a[e]||[]).forEach(function(t){t(e,n)})},off:function(t,e){return t?a[t]=e?(a[t]||[]).filter(function(t){return t!==e}):[]:a={},this}}}return function(t,e){var n=x(),a=S(t,function(t){n.emit(t,o)},c(e)),o={get state(){return a.state},on:n.on,off:n.off,setState:a.setState,open:a.open,close:a.close,destroy:a.destroy};return o}}); -------------------------------------------------------------------------------- /dist/tiny-date-picker.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["dist/tiny-date-picker.js"],"names":["global","factory","exports","module","define","amd","TinyDatePicker","this","now","dt","Date","setHours","datesEq","date1","date2","toDateString","shiftMonth","n","wrap","dayOfMonth","getDate","month","getMonth","setDate","setMonth","shiftYear","setFullYear","getFullYear","dateOrParse","parse","constrainDate","min","max","bufferFn","ms","fn","timeout","undefined","clearTimeout","setTimeout","noop","cp","args","arguments","o1","i","length","o2","key","english","days","months","today","clear","close","DatePickerOptions","opts","inRange","lang","mode","hilightedDate","format","str","date","isNaN","dateClass","dp","Key","left","up","right","down","enter","esc","on","evt","el","handler","addEventListener","removeEventListener","CustomEvent","window","event","params","bubbles","cancelable","detail","document","createEvent","initCustomEvent","prototype","Event","shimCustomEvent","views","day","onKeyDown","e","keyCode","shiftBy","preventDefault","setState","state","onClick","dp-day","selectedDate","parseInt","target","getAttribute","dp-next","dp-prev","dp-today","dp-clear","dp-close","dp-cal-month","view","dp-cal-year","render","dayNames","dayOffset","hilightedMonth","getTime","map","name","join","currentDate","result","iter","getDay","mapDays","isNotInMonth","isDisabled","isToday","className","year","currentYear","selectedYear","mapYears","shiftedYear","dp-year","dp-month","currentMonth","BaseMode","input","emit","detatchInputEvents","bufferShow","off","closing","shouldFocusOnBlur","shouldFocusOnRender","updateInput","adjustPosition","containerHTML","attachToDom","body","appendChild","simulated","value","dispatchEvent","computeSelectedDate","currentView","open","createElement","innerHTML","calEl","querySelector","split","forEach","ontouchstart","hasFocus","focus","activeElement","focusCurrent","attachContainerEvents","isVisible","parentNode","focused","contains","indexOf","shouldHide","becauseOfBlur","parent","removeChild","test","navigator","userAgent","MSStream","blur","destroy","firstChild","hadFocus","html","f","current","DropdownMode","Object","defineProperty","get","inputPos","win","cal","scrollTop","inputTop","calHeight","belowTop","aboveTop","isAbove","top","scrollLeft","inputLeft","maxRight","offsetWidth","shiftedLeft","getBoundingClientRect","pageYOffset","offsetHeight","height","innerHeight","classList","toggle","style","pageXOffset","innerWidth","visibility","Mode","tagName","readonly","root","Emitter","handlers","onOne","push","fns","onMany","arg","filter","h","emitter","me"],"mappings":"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","file":"./dist/tiny-date-picker.min.js.map"} -------------------------------------------------------------------------------- /docs/date-range-picker.md: -------------------------------------------------------------------------------- 1 | # Date Range Picker 2 | 3 | A date picker that supports ranges. This is an alpha release. The API will get 4 | extended with time, and possibly break. 5 | 6 | - Zero dependencies 7 | - Roughly 4KB minified and gzipped 8 | - IE9+ 9 | - Mobile-friendly/responsive 10 | - Supports multiple languages 11 | 12 | [See the demo...](http://chrisdavies.github.io/tiny-date-picker) 13 | 14 | ## Installation 15 | 16 | npm install --save tiny-date-picker 17 | 18 | ## Usage 19 | 20 | Include a reference to `tiny-date-picker.css` and `date-range-picker.js`, or import 21 | it `import {DateRangePicker} from 'tiny-date-picker/dist/date-range-picker';` then call it like this: 22 | 23 | ```javascript 24 | // Initialize a date range picker within the specified container 25 | DateRangePicker(document.querySelector('.container')); 26 | 27 | // Or with a CSS selector 28 | DateRangePicker('.container'); 29 | ``` 30 | 31 | Any options that you would have passed to TinyDatePicker can be passed to the DateRangePicker, but with a twist: 32 | 33 | ```javascript 34 | DateRangePicker('.cls', { 35 | startOpts: {}, // The options passed to the start date picker 36 | endOpts: {}, // The options passed to the end date picker 37 | }); 38 | ``` 39 | 40 | See [TinyDatePicker's docs](./tiny-date-picker.md) for more details. 41 | 42 | 43 | ## DateRangePicker object 44 | 45 | The DateRangePicker context is returned from the `DateRangePicker` function, and can be used to manipulate the date picker as documented below: 46 | 47 | ```javascript 48 | // Initialize a date picker in the specified container 49 | const dp = DateRangePicker('.container'); 50 | 51 | // Get the start date (can be null) 52 | dp.state.start; 53 | 54 | // Get the end date (can be null) 55 | dp.state.end; 56 | 57 | // Add an event handler 58 | dp.on('statechange', (_, picker) => console.log(picker.state)); 59 | 60 | // Remove all event handlers (see the Events section for more information) 61 | dp.off(); 62 | 63 | // Update the date picker's state and redraw as necessary. 64 | // This example causes the date picker to select the specified start date 65 | // and end date. 66 | dp.setState({ 67 | start: new Date('10/12/2017'), 68 | end: new Date('10/14/2017'), 69 | }); 70 | 71 | ``` 72 | 73 | ## Events 74 | 75 | The DateRangePicker object has an `on` and `off` method which allows you to register and unregister various event handlers. 76 | 77 | - statechange: Fired when the date picker's state changes (start or end date is selected). 78 | 79 | The event handler is passed two arguments: the name of the event, and the date picker object. 80 | 81 | ```js 82 | // Log the selected date range any time it changes 83 | DateRangePicker('.container') 84 | .on('statechange', (_, dp) => console.log(`${dp.state.start}-${dp.state.end}`)); 85 | ``` 86 | 87 | To remove an event handler, you call the date picker's `off` method, as with TinyDatePicker. 88 | 89 | ## Style 90 | 91 | All CSS class names for TinyDatePicker begin with `dp-`, and all CSS class names for DateRangePicker begin with `dr-`. 92 | 93 | 94 | ## Using TinyDatePicker and DateRangePicker together 95 | 96 | If you are using both TinyDatePicker and DateRangePicker, the only thing you should 97 | include in your project is DateRangePicker, as it includes TinyDatePicker. This was done 98 | to keep the overall bundle size down. 99 | 100 | ```javascript 101 | // Do this 102 | import {TinyDatePicker, DateRangePicker} from 'tiny-date-picker/date-range-picker'; 103 | 104 | // Don't do this, as this will double-include tiny-date-picker. 105 | import TinyDatePicker from 'tiny-date-picker'; 106 | import {DateRangePicker} from 'tiny-date-picker/date-range-picker'; 107 | ``` 108 | 109 | ## Positioning, showing, hiding, etc. 110 | 111 | DateRangePicker is intentionally minimal. It does not show/hide or attach to inputs 112 | directly. This is left up to the consumer. [See the demo](http://chrisdavies.github.io/tiny-date-picker/range-picker) for an example of how 113 | this might be done. 114 | -------------------------------------------------------------------------------- /docs/tiny-date-picker.md: -------------------------------------------------------------------------------- 1 | # Tiny Date Picker 2 | 3 | A light-weight date picker with zero dependencies. 4 | 5 | - Zero dependencies 6 | - Roughly 3.5KB minified and gzipped 7 | - IE9+ 8 | - Mobile-friendly/responsive 9 | - Supports multiple languages 10 | 11 | [See the demo...](http://chrisdavies.github.io/tiny-date-picker/) 12 | 13 | 14 | ## Installation 15 | 16 | npm install --save tiny-date-picker 17 | 18 | ## Usage 19 | 20 | Include a reference to `tiny-date-picker.css` and `tiny-date-picker.js`, or import 21 | it `import TinyDatePicker from 'tiny-date-picker';` then call it like this: 22 | 23 | ```javascript 24 | // Initialize a date picker on the specified input element 25 | TinyDatePicker(document.querySelector('input')); 26 | 27 | // Or with a CSS selector 28 | TinyDatePicker('.some-class-or-id-or-whatever'); 29 | ``` 30 | 31 | You can also pass in options as an optional second argument: 32 | 33 | ```javascript 34 | // Initialize a date picker using truncated month names 35 | TinyDatePicker(document.querySelector('input'), { 36 | lang: { 37 | months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 38 | }, 39 | }); 40 | ``` 41 | 42 | ## DatePicker object 43 | 44 | The DatePicker context is returned from the `TinyDatePicker` function, and can be used to 45 | manipulate the date picker as documented below: 46 | 47 | ```javascript 48 | // Initialize a date picker on the specified input element 49 | const dp = TinyDatePicker('input'); 50 | 51 | // Show the date picker 52 | dp.open(); 53 | 54 | // Hide the date picker (does nothing if the date picker is in permanent mode) 55 | dp.close(); 56 | 57 | // Get the current view of the date picker. Possible values are: 58 | // - 'day': The calendar is showing the day picker (the default) 59 | // - 'month': The calendar is showing the month picker 60 | // - 'year': The calendar is showing the year picker 61 | dp.state.view; 62 | 63 | // Get the currently selected date (can be null) 64 | dp.state.selectedDate; 65 | 66 | // Get the currently hilighted date (should not be null) 67 | dp.state.hilightedDate; 68 | 69 | // Add an event handler 70 | dp.on('statechange', (_, picker) => console.log(picker.state)); 71 | 72 | // Remove all event handlers (see the Events section for more information) 73 | dp.off(); 74 | 75 | // Update the date picker's state and redraw as necessary. 76 | // This example causes the date picker to show the month-picker view. 77 | // You can use setStsate to change the selectedDate or hilightedDate as well. 78 | dp.setState({ 79 | view: 'month', 80 | }); 81 | 82 | // Close the date picker and remove all event handlers from the input 83 | dp.destroy() 84 | 85 | ``` 86 | 87 | ## Options 88 | 89 | TinyDatePicker can be configured by passing it a second argument: 90 | 91 | ```javascript 92 | 93 | TinyDatePicker('input', { 94 | // What dom element the date picker will be added to. This defaults 95 | // to document.body 96 | appendTo: document.querySelector('.foo'), 97 | 98 | // Lang can be used to customize the text that is displayed 99 | // in the calendar. You can use this to display a different language. 100 | lang: { 101 | days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 102 | months: [ 103 | 'January', 104 | 'February', 105 | 'March', 106 | 'April', 107 | 'May', 108 | 'June', 109 | 'July', 110 | 'August', 111 | 'September', 112 | 'October', 113 | 'November', 114 | 'December', 115 | ], 116 | today: 'Today', 117 | clear: 'Clear', 118 | close: 'Close', 119 | }, 120 | 121 | // format {Date} -> string is a function which takes a date and returns a string. It can be used to customize 122 | // the way a date will look in the input after the user has selected it, and is particularly 123 | // useful if you're targeting a non-US customer. 124 | format(date) { 125 | return date.toLocaleDateString(); 126 | }, 127 | 128 | // parse {string|Date} -> Date is the inverse of format. If you specify one, you probably should specify the other 129 | // the default parse function handles whatever the new Date constructor handles. Note that 130 | // parse may be passed either a string or a date. 131 | parse(str) { 132 | var date = new Date(str); 133 | return isNaN(date) ? new Date() : date; 134 | }, 135 | 136 | // mode {'dp-modal'|'dp-below'|'dp-permanent'} specifies the way the date picker should display: 137 | // 'dp-modal' displays the picker as a modal 138 | // 'dp-below' displays the date picker as a dropdown 139 | // 'dp-permanent' displays the date picker as a permanent (always showing) calendar 140 | mode: 'dp-modal', 141 | 142 | // hilightedDate specifies what date to hilight when the date picker is displayed and the 143 | // associated input has no value. 144 | hilightedDate: new Date(), 145 | 146 | // min {string|Date} specifies the minimum date that can be selected (inclusive). 147 | // All earlier dates will be disabled. 148 | min: '10/1/2016', 149 | 150 | // max {string|Date} specifies the maximum date that can be selected (inclusive). 151 | // All later dates will be disabled. 152 | max: '10/1/2020', 153 | 154 | // inRange {Date} -> boolean takes a date and returns true or false. If false, the date 155 | // will be disabled in the date picker. 156 | inRange(dt) { 157 | return dt.getFullYear() % 2 > 0; 158 | }, 159 | 160 | // dateClass {Date} -> string takes a date and returns a CSS class name to be associated 161 | // with that date in the date picker. 162 | dateClass(dt) { 163 | return dt.getFullYear() % 2 ? 'odd-date' : 'even-date'; 164 | }, 165 | 166 | // dayOffset {number} specifies which day of the week is considered the first. By default, 167 | // this is 0 (Sunday). Set it to 1 for Monday, 2 for Tuesday, etc. 168 | dayOffset: 1 169 | }) 170 | 171 | ``` 172 | 173 | ## Events 174 | 175 | The input to which the date picker is attached will fire its `change` event 176 | any time the date value changes. 177 | 178 | The DatePicker object has an `on` and `off` method which allows you to register and unregister various event handlers. 179 | 180 | - open: Fired when the date picker opens / is shown 181 | - close: Fired when the date picker closes / is hidden 182 | - statechange: Fired when the date picker's state changes (view changes, hilighted date changes, selected date changes) 183 | - select: Fired when hte date picker's selected date changes (e.g. when the user picks a date) 184 | 185 | The event handler is passed two arguments: the name of the event, and the date picker object. 186 | 187 | ```js 188 | // Log the selected date any time it changes 189 | TinyDatePicker('.my-input') 190 | .on('select', (_, dp) => console.log(dp.state.selectedDate)) 191 | .on('close', () => console.log('CLOSED!!!')); 192 | 193 | // You can also register for multiple events at once without chaining the on method: 194 | TinyDatePicker('.my-input') 195 | .on({ 196 | select: (_, dp) => console.log(dp.state.selectedDate), 197 | close: () => console.log('CLOSED!!!') 198 | }); 199 | ``` 200 | 201 | To remove an event handler, you call the date picker's `off` method. 202 | 203 | ```js 204 | const dp = TinyDatePicker('.example'); 205 | 206 | function onOpen() { 207 | console.log('OPENED!!!'); 208 | } 209 | 210 | dp.on('open', onOpen); 211 | 212 | // Remove this specific open event handler 213 | dp.off('open', onOpen); 214 | 215 | // Remove all handlers of the open event 216 | dp.off('open'); 217 | 218 | // Remove all handlers of any event 219 | dp.off(); 220 | 221 | ``` 222 | 223 | ## Style 224 | 225 | All CSS class names begin with `dp-`, and every element in the calendar has a class. The style rules 226 | in `tiny-date-picker.css` have been kept as unspecific as possible so they can be easily overruled. 227 | 228 | For more info, launch a date picker and use the browser dev tools to inspect its structure and shape. 229 | 230 | ## Aria 231 | 232 | There is currently no Aria support baked into Tiny Date Picker, but it is planned. 233 | 234 | ## Bundling 235 | 236 | This library is [CommonJS](http://www.commonjs.org/) compatible, so you can use it in this way: 237 | 238 | ```javascript 239 | var TinyDatePicker = require('tiny-date-picker'), 240 | 241 | TinyDatePicker('.my-input'); 242 | ``` 243 | 244 | Or, with ES6: 245 | 246 | ```javascript 247 | import TinyDatePicker from 'tiny-date-picker', 248 | 249 | TinyDatePicker('.my-input'); 250 | ``` 251 | 252 | ## DateRanges 253 | 254 | If you want to pick date ranges, see [DateRangePicker](./date-range-picker.md). 255 | 256 | ## Version 2.x 257 | 258 | If you're using version 2.x, the docs are [here](https://github.com/chrisdavies/tiny-date-picker/tree/dev/v2). 259 | 260 | Migration to 3.x is documented [here](https://github.com/chrisdavies/tiny-date-picker/wiki/Changes-from-2.0-to-3.0). 261 | 262 | ## Contributing 263 | 264 | TinyDatePicker supports IE9+, and does so without need for a transpiler (Babel or TypeScript). So when 265 | contributing, be sure to write plain old vanilla ES3. 266 | 267 | Make sure all tests are passing: 268 | 269 | - Put the [Chrome webdriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) somewhere in your path. 270 | - Run `npm start` 271 | - In a new terminal tab/window, run `npm test` 272 | 273 | If all is well, build your changes: 274 | 275 | npm run min 276 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "allowSyntheticDefaultImports": true 6 | }, 7 | "exclude": [ 8 | "node_modules", 9 | "bower_components", 10 | "jspm_packages", 11 | "tmp", 12 | "temp" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-date-picker", 3 | "version": "3.2.8", 4 | "description": "A small, dependency-free date picker", 5 | "main": "dist/tiny-date-picker.js", 6 | "scripts": { 7 | "start": "concurrently \"npm run build:watch\" \"npm run serve\"", 8 | "selenium": "concurrently \"java -jar selenium-server-standalone-3.0.1.jar\" \"npm start\"", 9 | "serve": "http-server . -c-1 -p 8080", 10 | "test": "jest", 11 | "build": "concurrently \"npm run build:dr\" \"npm run build:dp\"", 12 | "build:watch": "concurrently \"npm run build:dr -- --watch\" \"npm run build:dp -- --watch\"", 13 | "build:dr": "rollup src/date-range-picker.js --output.format umd --name DateRangePicker --output.file dist/date-range-picker.js", 14 | "build:dp": "rollup src/index.js --output.format umd --name TinyDatePicker --output.file dist/tiny-date-picker.js", 15 | "min": "concurrently \"npm run min:dpcss\" \"npm run min:dpjs\" \"npm run min:rpcss\" \"npm run min:rpjs\"", 16 | "min:dpcss": "uglifycss tiny-date-picker.css > tiny-date-picker.min.css && echo 'Minified CSS:' && cat tiny-date-picker.min.css | gzip -9f | wc -c", 17 | "min:dpjs": "uglifyjs dist/tiny-date-picker.js --source-map \"filename='./dist/tiny-date-picker.min.js.map'\" -m -c -o dist/tiny-date-picker.min.js && echo 'Minified JS:' && cat dist/tiny-date-picker.min.js | gzip -9f | wc -c", 18 | "min:rpcss": "uglifycss date-range-picker.css > date-range-picker.min.css && echo 'Minified CSS:' && cat date-range-picker.min.css | gzip -9f | wc -c", 19 | "min:rpjs": "uglifyjs dist/date-range-picker.js --source-map \"filename='./dist/date-range-picker.min.js.map'\" -m -c -o dist/date-range-picker.min.js && echo 'Minified JS:' && cat dist/date-range-picker.min.js | gzip -9f | wc -c" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/chrisdavies/tiny-date-picker.git" 24 | }, 25 | "keywords": [ 26 | "date", 27 | "picker", 28 | "date", 29 | "chooser", 30 | "calendar" 31 | ], 32 | "author": "Chris Davies ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/chrisdavies/tiny-date-picker/issues" 36 | }, 37 | "jest": { 38 | "testRegex": ".*\\.test\\.js$", 39 | "moduleFileExtensions": [ 40 | "js" 41 | ] 42 | }, 43 | "homepage": "https://github.com/chrisdavies/tiny-date-picker#readme", 44 | "devDependencies": { 45 | "babel": "^6.23.0", 46 | "babel-jest": "^22.4.3", 47 | "babel-preset-es2015": "^6.24.1", 48 | "concurrently": "^3.5.1", 49 | "http-server": "^0.11.1", 50 | "jest": "^22.4.2", 51 | "rollup": "^0.57.1", 52 | "selenium-webdriver": "^4.0.0-alpha.1", 53 | "uglify-js": "^3.3.16", 54 | "uglifycss": "0.0.29" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Tiny Date Picker 2 | 3 | A light-weight date picker with zero dependencies. 4 | 5 | - Zero dependencies 6 | - Roughly 3.5KB minified and gzipped 7 | - IE9+ 8 | - Mobile-friendly/responsive 9 | - Supports multiple languages 10 | - [TinyDatePicker demo](https://chrisdavies.github.io/tiny-date-picker/) 11 | 12 | ## Installation 13 | 14 | npm install --save tiny-date-picker 15 | 16 | ## Usage 17 | 18 | Include a reference to `tiny-date-picker.css` and `tiny-date-picker.js`, or import 19 | it `import TinyDatePicker from 'tiny-date-picker';` then call it like this: 20 | 21 | ```javascript 22 | // Initialize a date picker on the specified input element 23 | TinyDatePicker(document.querySelector('input')); 24 | 25 | // Or with a CSS selector 26 | TinyDatePicker('.some-class-or-id-or-whatever'); 27 | ``` 28 | 29 | ## Documentation 30 | 31 | - [TinyDatePicker documentation](./docs/tiny-date-picker.md) 32 | - [DateRangePicker documentation](./docs/date-range-picker.md) 33 | 34 | ## License MIT 35 | 36 | Copyright (c) 2015 Chris Davies 37 | 38 | Permission is hereby granted, free of charge, to any person 39 | obtaining a copy of this software and associated documentation 40 | files (the "Software"), to deal in the Software without 41 | restriction, including without limitation the rights to use, 42 | copy, modify, merge, publish, distribute, sublicense, and/or sell 43 | copies of the Software, and to permit persons to whom the 44 | Software is furnished to do so, subject to the following 45 | conditions: 46 | 47 | The above copyright notice and this permission notice shall be 48 | included in all copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 51 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 52 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 53 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 54 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 55 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 56 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 57 | OTHER DEALINGS IN THE SOFTWARE. 58 | -------------------------------------------------------------------------------- /src/date-picker-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Responsible for sanitizing and creating date picker options. 3 | */ 4 | 5 | import {now, shiftYear, dateOrParse} from './lib/date-manip'; 6 | import {cp} from './lib/fns'; 7 | 8 | var english = { 9 | days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 10 | months: [ 11 | 'January', 12 | 'February', 13 | 'March', 14 | 'April', 15 | 'May', 16 | 'June', 17 | 'July', 18 | 'August', 19 | 'September', 20 | 'October', 21 | 'November', 22 | 'December', 23 | ], 24 | today: 'Today', 25 | clear: 'Clear', 26 | close: 'Close', 27 | }; 28 | 29 | /** 30 | * DatePickerOptions constructs a new date picker options object, overriding 31 | * default values with any values specified in opts. 32 | * 33 | * @param {DatePickerOptions} opts 34 | * @returns {DatePickerOptions} 35 | */ 36 | export default function DatePickerOptions(opts) { 37 | opts = opts || {}; 38 | opts = cp(defaults(), opts); 39 | var parse = dateOrParse(opts.parse); 40 | opts.lang = cp(english, opts.lang); 41 | opts.parse = parse; 42 | opts.inRange = makeInRangeFn(opts); 43 | opts.min = parse(opts.min || shiftYear(now(), -100)); 44 | opts.max = parse(opts.max || shiftYear(now(), 100)); 45 | opts.hilightedDate = opts.parse(opts.hilightedDate); 46 | 47 | return opts; 48 | } 49 | 50 | function defaults() { 51 | return { 52 | lang: english, 53 | 54 | // Possible values: dp-modal, dp-below, dp-permanent 55 | mode: 'dp-modal', 56 | 57 | // The date to hilight initially if the date picker has no 58 | // initial value. 59 | hilightedDate: now(), 60 | 61 | format: function (dt) { 62 | return (dt.getMonth() + 1) + '/' + dt.getDate() + '/' + dt.getFullYear(); 63 | }, 64 | 65 | parse: function (str) { 66 | var date = new Date(str); 67 | return isNaN(date) ? now() : date; 68 | }, 69 | 70 | dateClass: function () { }, 71 | 72 | inRange: function () { 73 | return true; 74 | }, 75 | 76 | appendTo: document.body, 77 | }; 78 | } 79 | 80 | function makeInRangeFn(opts) { 81 | var inRange = opts.inRange; // Cache this version, and return a variant 82 | 83 | return function (dt, dp) { 84 | return inRange(dt, dp) && opts.min <= dt && opts.max >= dt; 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/date-range-picker.js: -------------------------------------------------------------------------------- 1 | // A date range picker built on top of TinyDatePicker; 2 | import TDP from './index'; 3 | import Emitter from './lib/emitter'; 4 | import {shiftMonth, datesEq} from './lib/date-manip'; 5 | import {cp} from './lib/fns'; 6 | 7 | export var TinyDatePicker = TDP; 8 | 9 | /** 10 | * The state values for the date range picker 11 | * 12 | * @typedef {Object} DateRangeState 13 | * @property {Date} start - The start date (can be null) 14 | * @property {Date} end - The end date (can be null) 15 | */ 16 | 17 | /** 18 | * An instance of TinyDatePicker 19 | * 20 | * @typedef {Object} DateRangePickerInst 21 | * @property {DateRangeState} state - The start / end dates 22 | * @property {function} on - Adds an event handler 23 | * @property {function} off - Removes an event handler 24 | * @property {function} setState - Changes the current state of the date picker 25 | */ 26 | 27 | /** 28 | * TinyDatePicker constructs a new date picker for the specified input 29 | * 30 | * @param {HTMLElement} input The input associated with the datepicker 31 | * @returns {DateRangePickerInst} 32 | */ 33 | export function DateRangePicker(container, opts) { 34 | opts = opts || {}; 35 | var emitter = Emitter(); 36 | var root = renderInto(container); 37 | var hoverDate; 38 | var state = { 39 | start: undefined, 40 | end: undefined, 41 | }; 42 | var start = TDP(root.querySelector('.dr-cal-start'), cp({}, opts.startOpts, { 43 | mode: 'dp-permanent', 44 | dateClass: dateClass, 45 | })); 46 | var end = TDP(root.querySelector('.dr-cal-end'), cp({}, opts.endOpts, { 47 | mode: 'dp-permanent', 48 | hilightedDate: shiftMonth(start.state.hilightedDate, 1), 49 | dateClass: dateClass, 50 | })); 51 | var handlers = { 52 | 'statechange': onStateChange, 53 | 'select': dateSelected, 54 | }; 55 | var me = { 56 | state: state, 57 | setState: setState, 58 | on: emitter.on, 59 | off: emitter.off, 60 | }; 61 | 62 | start.on(handlers); 63 | end.on(handlers); 64 | 65 | function onStateChange(_, dp) { 66 | var d1 = start.state.hilightedDate; 67 | var d2 = end.state.hilightedDate; 68 | var diff = diffMonths(d1, d2); 69 | 70 | if (diff === 1) { 71 | return; 72 | } 73 | 74 | if (dp === start) { 75 | end.setState({ 76 | hilightedDate: shiftMonth(dp.state.hilightedDate, 1), 77 | }); 78 | } else { 79 | start.setState({ 80 | hilightedDate: shiftMonth(dp.state.hilightedDate, -1), 81 | }); 82 | } 83 | } 84 | 85 | function dateSelected(_, dp) { 86 | var dt = dp.state.selectedDate; 87 | 88 | if (!state.start || state.end) { 89 | setState({ 90 | start: dt, 91 | end: undefined, 92 | }); 93 | } else { 94 | setState({ 95 | start: dt > state.start ? state.start : dt, 96 | end: dt > state.start ? dt : state.start, 97 | }); 98 | } 99 | }; 100 | 101 | function setState(newState) { 102 | for (var key in newState) { 103 | state[key] = newState[key]; 104 | } 105 | 106 | emitter.emit('statechange', me); 107 | rerender(); 108 | } 109 | 110 | function rerender() { 111 | start.setState({}); 112 | end.setState({}); 113 | } 114 | 115 | // Hack to avoid a situation where iOS requires double-clicking to select 116 | if (!/iPhone|iPad|iPod/i.test(navigator.userAgent)) { 117 | root.addEventListener('mouseover', function mouseOverDate(e) { 118 | if (e.target.classList.contains('dp-day')) { 119 | var dt = new Date(parseInt(e.target.dataset.date)); 120 | var changed = !datesEq(dt, hoverDate); 121 | 122 | if (changed) { 123 | hoverDate = dt; 124 | rerender(); 125 | } 126 | } 127 | }); 128 | } 129 | 130 | function dateClass(dt) { 131 | var rangeClass = (state.end || hoverDate) && 132 | state.start && 133 | inRange(dt, state.end || hoverDate, state.start); 134 | var selectedClass = datesEq(dt, state.start) || datesEq(dt, state.end); 135 | 136 | return (rangeClass ? 'dr-in-range ' : '') + 137 | (selectedClass ? 'dr-selected ' : ''); 138 | } 139 | 140 | return me; 141 | } 142 | 143 | function renderInto(container) { 144 | if (typeof container === 'string') { 145 | container = document.querySelector(container); 146 | } 147 | 148 | container.innerHTML = '
' + 149 | '
' + 150 | '
' + 151 | '
'; 152 | 153 | return container.querySelector('.dr-cals'); 154 | } 155 | 156 | function toMonths(dt) { 157 | return (dt.getYear() * 12) + dt.getMonth(); 158 | } 159 | 160 | function diffMonths(d1, d2) { 161 | return toMonths(d2) - toMonths(d1); 162 | } 163 | 164 | function inRange(dt, start, end) { 165 | return (dt < end && dt >= start) || (dt <= start && dt > end); 166 | } 167 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file The root date picker file, defines public exports for the library. 3 | */ 4 | 5 | import DatePickerOptions from './date-picker-options'; 6 | import Mode from './mode/index'; 7 | import Emitter from './lib/emitter'; 8 | 9 | /** 10 | * The date picker language configuration 11 | * @typedef {Object} LangOptions 12 | * @property {Array.} [days] - Days of the week 13 | * @property {Array.} [months] - Months of the year 14 | * @property {string} today - The label for the 'today' button 15 | * @property {string} close - The label for the 'close' button 16 | * @property {string} clear - The label for the 'clear' button 17 | */ 18 | 19 | /** 20 | * The configuration options for a date picker. 21 | * 22 | * @typedef {Object} DatePickerOptions 23 | * @property {LangOptions} [lang] - Configures the label text, defaults to English 24 | * @property {('dp-modal'|'dp-below'|'dp-permanent')} [mode] - The date picker mode, defaults to 'dp-modal' 25 | * @property {(string|Date)} [hilightedDate] - The date to hilight if no date is selected 26 | * @property {function(string|Date):Date} [parse] - Parses a date, the complement of the "format" function 27 | * @property {function(Date):string} [format] - Formats a date for displaying to user 28 | * @property {function(Date):string} [dateClass] - Associates a custom CSS class with a date 29 | * @property {function(Date):boolean} [inRange] - Indicates whether or not a date is selectable 30 | * @property {(string|Date)} [min] - The minimum selectable date (inclusive, default 100 years ago) 31 | * @property {(string|Date)} [max] - The maximum selectable date (inclusive, default 100 years from now) 32 | */ 33 | 34 | /** 35 | * The state values for the date picker 36 | * 37 | * @typedef {Object} DatePickerState 38 | * @property {string} view - The current view 'day' | 'month' | 'year' 39 | * @property {Date} selectedDate - The date which has been selected by the user 40 | * @property {Date} hilightedDate - The date which is currently hilighted / active 41 | */ 42 | 43 | /** 44 | * An instance of TinyDatePicker 45 | * 46 | * @typedef {Object} DatePicker 47 | * @property {DatePickerState} state - The values currently displayed. 48 | * @property {function} on - Adds an event handler 49 | * @property {function} off - Removes an event handler 50 | * @property {function} setState - Changes the current state of the date picker 51 | * @property {function} open - Opens the date picker 52 | * @property {function} close - Closes the date picker 53 | * @property {function} destroy - Destroys the date picker (removing all handlers from the input, too) 54 | */ 55 | 56 | /** 57 | * TinyDatePicker constructs a new date picker for the specified input 58 | * 59 | * @param {HTMLElement | string} input The input or CSS selector associated with the datepicker 60 | * @param {DatePickerOptions} opts The options for initializing the date picker 61 | * @returns {DatePicker} 62 | */ 63 | export default function TinyDatePicker(input, opts) { 64 | var emitter = Emitter(); 65 | var options = DatePickerOptions(opts); 66 | var mode = Mode(input, emit, options); 67 | var me = { 68 | get state() { 69 | return mode.state; 70 | }, 71 | on: emitter.on, 72 | off: emitter.off, 73 | setState: mode.setState, 74 | open: mode.open, 75 | close: mode.close, 76 | destroy: mode.destroy, 77 | }; 78 | 79 | function emit(evt) { 80 | emitter.emit(evt, me); 81 | } 82 | 83 | return me; 84 | } 85 | -------------------------------------------------------------------------------- /src/lib/date-manip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file A generic set of mutation-free date functions. 3 | */ 4 | 5 | /** 6 | * now returns the current date without any time values 7 | * 8 | * @returns {Date} 9 | */ 10 | export function now() { 11 | var dt = new Date(); 12 | dt.setHours(0, 0, 0, 0); 13 | return dt; 14 | } 15 | 16 | /** 17 | * dateEq compares two dates 18 | * 19 | * @param {Date} date1 the first date 20 | * @param {Date} date2 the second date 21 | * @returns {boolean} 22 | */ 23 | export function datesEq(date1, date2) { 24 | return (date1 && date1.toDateString()) === (date2 && date2.toDateString()); 25 | } 26 | 27 | /** 28 | * shiftDay shifts the specified date by n days 29 | * 30 | * @param {Date} dt 31 | * @param {number} n 32 | * @returns {Date} 33 | */ 34 | export function shiftDay(dt, n) { 35 | dt = new Date(dt); 36 | dt.setDate(dt.getDate() + n); 37 | return dt; 38 | } 39 | 40 | /** 41 | * shiftMonth shifts the specified date by a specified number of months 42 | * 43 | * @param {Date} dt 44 | * @param {number} n 45 | * @param {boolean} wrap optional, if true, does not change year 46 | * value, defaults to false 47 | * @returns {Date} 48 | */ 49 | export function shiftMonth(dt, n, wrap) { 50 | dt = new Date(dt); 51 | 52 | var dayOfMonth = dt.getDate(); 53 | var month = dt.getMonth() + n; 54 | 55 | dt.setDate(1); 56 | dt.setMonth(wrap ? (12 + month) % 12 : month); 57 | dt.setDate(dayOfMonth); 58 | 59 | // If dayOfMonth = 31, but the target month only has 30 or 29 or whatever... 60 | // head back to the max of the target month 61 | if (dt.getDate() < dayOfMonth) { 62 | dt.setDate(0); 63 | } 64 | 65 | return dt; 66 | } 67 | 68 | /** 69 | * shiftYear shifts the specified date by n years 70 | * 71 | * @param {Date} dt 72 | * @param {number} n 73 | * @returns {Date} 74 | */ 75 | export function shiftYear(dt, n) { 76 | dt = new Date(dt); 77 | dt.setFullYear(dt.getFullYear() + n); 78 | return dt; 79 | } 80 | 81 | /** 82 | * setYear changes the specified date to the specified year 83 | * 84 | * @param {Date} dt 85 | * @param {number} year 86 | */ 87 | export function setYear(dt, year) { 88 | dt = new Date(dt); 89 | dt.setFullYear(year); 90 | return dt; 91 | } 92 | 93 | /** 94 | * setMonth changes the specified date to the specified month 95 | * 96 | * @param {Date} dt 97 | * @param {number} month 98 | */ 99 | export function setMonth(dt, month) { 100 | return shiftMonth(dt, month - dt.getMonth()); 101 | } 102 | 103 | /** 104 | * dateOrParse creates a function which, given a date or string, returns a date 105 | * 106 | * @param {function} parse the function used to parse strings 107 | * @returns {function} 108 | */ 109 | export function dateOrParse(parse) { 110 | return function (dt) { 111 | return dropTime(typeof dt === 'string' ? parse(dt) : dt); 112 | }; 113 | } 114 | 115 | /** 116 | * constrainDate returns dt or min/max depending on whether dt is out of bounds (inclusive) 117 | * 118 | * @export 119 | * @param {Date} dt 120 | * @param {Date} min 121 | * @param {Date} max 122 | * @returns {Date} 123 | */ 124 | export function constrainDate(dt, min, max) { 125 | return (dt < min) ? min : 126 | (dt > max) ? max : 127 | dt; 128 | } 129 | 130 | function dropTime(dt) { 131 | dt = new Date(dt); 132 | dt.setHours(0, 0, 0, 0); 133 | return dt; 134 | } 135 | -------------------------------------------------------------------------------- /src/lib/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Helper functions for dealing with dom elements. 3 | */ 4 | 5 | export var Key = { 6 | left: 37, 7 | up: 38, 8 | right: 39, 9 | down: 40, 10 | enter: 13, 11 | esc: 27, 12 | }; 13 | 14 | /** 15 | * on attaches an event handler to the specified element, and returns an 16 | * off function which can be used to remove the handler. 17 | * 18 | * @param {string} evt the name of the event to handle 19 | * @param {HTMLElement} el the element to attach to 20 | * @param {function} handler the event handler 21 | * @returns {function} the off function 22 | */ 23 | export function on(evt, el, handler) { 24 | el.addEventListener(evt, handler, true); 25 | 26 | return function () { 27 | el.removeEventListener(evt, handler, true); 28 | }; 29 | } 30 | 31 | export var CustomEvent = shimCustomEvent(); 32 | 33 | function shimCustomEvent() { 34 | var CustomEvent = window.CustomEvent; 35 | 36 | if (typeof CustomEvent !== 'function') { 37 | CustomEvent = function (event, params) { 38 | params = params || {bubbles: false, cancelable: false, detail: undefined}; 39 | var evt = document.createEvent('CustomEvent'); 40 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 41 | return evt; 42 | }; 43 | 44 | CustomEvent.prototype = window.Event.prototype; 45 | } 46 | 47 | return CustomEvent; 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/emitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Defines simple event emitter behavior. 3 | */ 4 | 5 | /** 6 | * Emitter constructs a new emitter object which has on/off methods. 7 | * 8 | * @returns {EventEmitter} 9 | */ 10 | export default function Emitter() { 11 | var handlers = {}; 12 | 13 | function onOne(name, handler) { 14 | (handlers[name] = (handlers[name] || [])).push(handler); 15 | } 16 | 17 | function onMany(fns) { 18 | for (var name in fns) { 19 | onOne(name, fns[name]); 20 | } 21 | } 22 | 23 | return { 24 | on: function (name, handler) { 25 | if (handler) { 26 | onOne(name, handler); 27 | } else { 28 | onMany(name); 29 | } 30 | 31 | return this; 32 | }, 33 | 34 | emit: function (name, arg) { 35 | (handlers[name] || []).forEach(function (handler) { 36 | handler(name, arg); 37 | }); 38 | }, 39 | 40 | off: function (name, handler) { 41 | if (!name) { 42 | handlers = {}; 43 | } else if (!handler) { 44 | handlers[name] = []; 45 | } else { 46 | handlers[name] = (handlers[name] || []).filter(function (h) { 47 | return h !== handler; 48 | }); 49 | } 50 | 51 | return this; 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/fns.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Utility functions for function manipulation. 3 | */ 4 | 5 | /** 6 | * bufferFn buffers calls to fn so they only happen every ms milliseconds 7 | * 8 | * @param {number} ms number of milliseconds 9 | * @param {function} fn the function to be buffered 10 | * @returns {function} 11 | */ 12 | export function bufferFn(ms, fn) { 13 | var timeout = undefined; 14 | return function () { 15 | clearTimeout(timeout); 16 | timeout = setTimeout(fn, ms); 17 | }; 18 | } 19 | 20 | /** 21 | * noop is a function which does nothing at all. 22 | */ 23 | export function noop() { } 24 | 25 | /** 26 | * copy properties from object o2 to object o1. 27 | * 28 | * @params {Object} o1 29 | * @params {Object} o2 30 | * @returns {Object} 31 | */ 32 | export function cp() { 33 | var args = arguments; 34 | var o1 = args[0]; 35 | for (var i = 1; i < args.length; ++i) { 36 | var o2 = args[i] || {}; 37 | for (var key in o2) { 38 | o1[key] = o2[key]; 39 | } 40 | } 41 | return o1; 42 | } 43 | -------------------------------------------------------------------------------- /src/mode/base-mode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Defines the base date picker behavior, overridden by various modes. 3 | */ 4 | import dayPicker from '../views/day-picker'; 5 | import monthPicker from '../views/month-picker'; 6 | import yearPicker from '../views/year-picker'; 7 | import {bufferFn, noop} from '../lib/fns'; 8 | import {on, CustomEvent, Key} from '../lib/dom'; 9 | import {constrainDate} from '../lib/date-manip'; 10 | 11 | var views = { 12 | day: dayPicker, 13 | year: yearPicker, 14 | month: monthPicker 15 | }; 16 | 17 | export default function BaseMode(input, emit, opts) { 18 | var detatchInputEvents; // A function that detaches all events from the input 19 | var closing = false; // A hack to prevent calendar from re-opening when closing. 20 | var selectedDate; // The currently selected date 21 | var dp = { 22 | // The root DOM element for the date picker, initialized on first open. 23 | el: undefined, 24 | opts: opts, 25 | shouldFocusOnBlur: true, 26 | shouldFocusOnRender: true, 27 | state: initialState(), 28 | adjustPosition: noop, 29 | containerHTML: '
', 30 | 31 | attachToDom: function () { 32 | var appendTo = opts.appendTo || document.body; 33 | appendTo.appendChild(dp.el); 34 | }, 35 | 36 | updateInput: function (selectedDate) { 37 | var e = new CustomEvent('change', {bubbles: true}); 38 | e.simulated = true; 39 | input.value = selectedDate ? opts.format(selectedDate) : ''; 40 | input.dispatchEvent(e); 41 | }, 42 | 43 | computeSelectedDate: function () { 44 | return opts.parse(input.value); 45 | }, 46 | 47 | currentView: function() { 48 | return views[dp.state.view]; 49 | }, 50 | 51 | open: function () { 52 | if (closing) { 53 | return; 54 | } 55 | 56 | if (!dp.el) { 57 | dp.el = createContainerElement(opts, dp.containerHTML); 58 | attachContainerEvents(dp); 59 | } 60 | 61 | selectedDate = constrainDate(dp.computeSelectedDate(), opts.min, opts.max); 62 | dp.state.hilightedDate = selectedDate || opts.hilightedDate; 63 | dp.state.view = 'day'; 64 | 65 | dp.attachToDom(); 66 | dp.render(); 67 | 68 | emit('open'); 69 | }, 70 | 71 | isVisible: function () { 72 | return !!dp.el && !!dp.el.parentNode; 73 | }, 74 | 75 | hasFocus: function () { 76 | var focused = document.activeElement; 77 | return dp.el && 78 | dp.el.contains(focused) && 79 | focused.className.indexOf('dp-focuser') < 0; 80 | }, 81 | 82 | shouldHide: function () { 83 | return dp.isVisible(); 84 | }, 85 | 86 | close: function (becauseOfBlur) { 87 | var el = dp.el; 88 | 89 | if (!dp.isVisible()) { 90 | return; 91 | } 92 | 93 | if (el) { 94 | var parent = el.parentNode; 95 | parent && parent.removeChild(el); 96 | } 97 | 98 | closing = true; 99 | 100 | if (becauseOfBlur && dp.shouldFocusOnBlur) { 101 | focusInput(input); 102 | } 103 | 104 | // When we close, the input often gains refocus, which 105 | // can then launch the date picker again, so we buffer 106 | // a bit and don't show the date picker within N ms of closing 107 | setTimeout(function() { 108 | closing = false; 109 | }, 100); 110 | 111 | emit('close'); 112 | }, 113 | 114 | destroy: function () { 115 | dp.close(); 116 | detatchInputEvents(); 117 | }, 118 | 119 | render: function () { 120 | if (!dp.el) { 121 | return; 122 | } 123 | 124 | var hadFocus = dp.hasFocus(); 125 | var html = dp.currentView().render(dp); 126 | html && (dp.el.firstChild.innerHTML = html); 127 | 128 | dp.adjustPosition(); 129 | 130 | if (hadFocus || dp.shouldFocusOnRender) { 131 | focusCurrent(dp); 132 | } 133 | }, 134 | 135 | // Conceptually similar to setState in React, updates 136 | // the view state and re-renders. 137 | setState: function (state) { 138 | for (var key in state) { 139 | dp.state[key] = state[key]; 140 | } 141 | 142 | emit('statechange'); 143 | dp.render(); 144 | }, 145 | }; 146 | 147 | detatchInputEvents = attachInputEvents(input, dp); 148 | 149 | // Builds the initial view state 150 | // selectedDate is a special case and causes changes to hilightedDate 151 | // hilightedDate is set on open, so remains undefined initially 152 | // view is the current view (day, month, year) 153 | function initialState() { 154 | return { 155 | get selectedDate() { 156 | return selectedDate; 157 | }, 158 | set selectedDate(dt) { 159 | if (dt && !opts.inRange(dt)) { 160 | return; 161 | } 162 | 163 | if (dt) { 164 | selectedDate = new Date(dt); 165 | dp.state.hilightedDate = selectedDate; 166 | } else { 167 | selectedDate = dt; 168 | } 169 | 170 | dp.updateInput(selectedDate); 171 | emit('select'); 172 | dp.close(); 173 | }, 174 | view: 'day', 175 | }; 176 | } 177 | 178 | return dp; 179 | } 180 | 181 | function createContainerElement(opts, containerHTML) { 182 | var el = document.createElement('div'); 183 | 184 | el.className = opts.mode; 185 | el.innerHTML = containerHTML; 186 | 187 | return el; 188 | } 189 | 190 | function attachInputEvents(input, dp) { 191 | var bufferShow = bufferFn(5, function () { 192 | if (dp.shouldHide()) { 193 | dp.close(); 194 | } else { 195 | dp.open(); 196 | } 197 | }); 198 | 199 | var off = [ 200 | on('blur', input, bufferFn(150, function () { 201 | if (!dp.hasFocus()) { 202 | dp.close(true); 203 | } 204 | })), 205 | 206 | on('mousedown', input, function () { 207 | if (input === document.activeElement) { 208 | bufferShow(); 209 | } 210 | }), 211 | 212 | on('focus', input, bufferShow), 213 | 214 | on('input', input, function (e) { 215 | var date = dp.opts.parse(e.target.value); 216 | isNaN(date) || dp.setState({ 217 | hilightedDate: date 218 | }); 219 | }), 220 | ]; 221 | 222 | // Unregister all events that were registered above. 223 | return function() { 224 | off.forEach(function (f) { 225 | f(); 226 | }); 227 | }; 228 | } 229 | 230 | function focusCurrent(dp) { 231 | var current = dp.el.querySelector('.dp-current'); 232 | return current && current.focus(); 233 | } 234 | 235 | function attachContainerEvents(dp) { 236 | var el = dp.el; 237 | var calEl = el.querySelector('.dp'); 238 | 239 | // Hack to get iOS to show active CSS states 240 | el.ontouchstart = noop; 241 | 242 | function onClick(e) { 243 | e.target.className.split(' ').forEach(function(evt) { 244 | var handler = dp.currentView().onClick[evt]; 245 | handler && handler(e, dp); 246 | }); 247 | } 248 | 249 | // The calender fires a blur event *every* time we redraw 250 | // this means we need to buffer the blur event to see if 251 | // it still has no focus after redrawing, and only then 252 | // do we return focus to the input. A possible other approach 253 | // would be to set context.redrawing = true on redraw and 254 | // set it to false in the blur event. 255 | on('blur', calEl, bufferFn(150, function () { 256 | if (!dp.hasFocus()) { 257 | dp.close(true); 258 | } 259 | })); 260 | 261 | on('keydown', el, function (e) { 262 | if (e.keyCode === Key.enter) { 263 | onClick(e); 264 | } else { 265 | dp.currentView().onKeyDown(e, dp); 266 | } 267 | }); 268 | 269 | // If the user clicks in non-focusable space, but 270 | // still within the date picker, we don't want to 271 | // hide, so we need to hack some things... 272 | on('mousedown', calEl, function (e) { 273 | e.target.focus && e.target.focus(); // IE hack 274 | if (document.activeElement !== e.target) { 275 | e.preventDefault(); 276 | focusCurrent(dp); 277 | } 278 | }); 279 | 280 | on('click', el, onClick); 281 | } 282 | 283 | function focusInput(input) { 284 | // When the modal closes, we need to focus the original input so the 285 | // user can continue tabbing from where they left off. 286 | input.focus(); 287 | 288 | // iOS zonks out if we don't blur the input, so... 289 | if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) { 290 | input.blur(); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/mode/dropdown-mode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Defines the dropdown date picker behavior. 3 | */ 4 | 5 | import BaseMode from './base-mode'; 6 | 7 | export default function DropdownMode(input, emit, opts) { 8 | var dp = BaseMode(input, emit, opts); 9 | 10 | dp.shouldFocusOnBlur = false; 11 | 12 | Object.defineProperty(dp, 'shouldFocusOnRender', { 13 | get: function() { 14 | return input !== document.activeElement; 15 | } 16 | }); 17 | 18 | dp.adjustPosition = function () { 19 | autoPosition(input, dp); 20 | }; 21 | 22 | return dp; 23 | } 24 | 25 | function autoPosition(input, dp) { 26 | var inputPos = input.getBoundingClientRect(); 27 | var win = window; 28 | 29 | adjustCalY(dp, inputPos, win); 30 | adjustCalX(dp, inputPos, win); 31 | 32 | dp.el.style.visibility = ''; 33 | } 34 | 35 | function adjustCalX(dp, inputPos, win) { 36 | var cal = dp.el; 37 | var scrollLeft = win.pageXOffset; 38 | var inputLeft = inputPos.left + scrollLeft; 39 | var maxRight = win.innerWidth + scrollLeft; 40 | var offsetWidth = cal.offsetWidth; 41 | var calRight = inputLeft + offsetWidth; 42 | var shiftedLeft = maxRight - offsetWidth; 43 | var left = calRight > maxRight && shiftedLeft > 0 ? shiftedLeft : inputLeft; 44 | 45 | cal.style.left = left + 'px'; 46 | } 47 | 48 | function adjustCalY(dp, inputPos, win) { 49 | var cal = dp.el; 50 | var scrollTop = win.pageYOffset; 51 | var inputTop = scrollTop + inputPos.top; 52 | var calHeight = cal.offsetHeight; 53 | var belowTop = inputTop + inputPos.height + 8; 54 | var aboveTop = inputTop - calHeight - 8; 55 | var isAbove = (aboveTop > 0 && belowTop + calHeight > scrollTop + win.innerHeight); 56 | var top = isAbove ? aboveTop : belowTop; 57 | 58 | if (cal.classList) { 59 | cal.classList.toggle('dp-is-above', isAbove); 60 | cal.classList.toggle('dp-is-below', !isAbove); 61 | } 62 | cal.style.top = top + 'px'; 63 | } 64 | -------------------------------------------------------------------------------- /src/mode/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Defines the various date picker modes (modal, dropdown, permanent) 3 | */ 4 | 5 | import ModalMode from './modal-mode'; 6 | import DropdownMode from './dropdown-mode'; 7 | import PermanentMode from './permanent-mode'; 8 | 9 | export default function Mode(input, emit, opts) { 10 | input = input && input.tagName ? input : document.querySelector(input); 11 | 12 | if (opts.mode === 'dp-modal') { 13 | return ModalMode(input, emit, opts); 14 | } 15 | 16 | if (opts.mode === 'dp-below') { 17 | return DropdownMode(input, emit, opts); 18 | } 19 | 20 | if (opts.mode === 'dp-permanent') { 21 | return PermanentMode(input, emit, opts); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/mode/modal-mode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Defines the modal date picker behavior. 3 | */ 4 | import BaseMode from './base-mode'; 5 | 6 | export default function ModalMode(input, emit, opts) { 7 | var dp = BaseMode(input, emit, opts); 8 | 9 | // In modal mode, users really shouldn't be able to type in 10 | // the input, as all input is done via the calendar. 11 | input.readonly = true; 12 | 13 | // In modal mode, we need to know when the user has tabbed 14 | // off the end of the calendar, and set focus to the original 15 | // input. To do this, we add a special element to the DOM. 16 | // When the user tabs off the bottom of the calendar, they 17 | // will tab onto this element. 18 | dp.containerHTML += '.'; 19 | 20 | return dp; 21 | } 22 | -------------------------------------------------------------------------------- /src/mode/permanent-mode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Defines the permanent date picker behavior. 3 | */ 4 | import {noop} from '../lib/fns'; 5 | import BaseMode from './base-mode'; 6 | 7 | export default function PermanentMode(root, emit, opts) { 8 | var dp = BaseMode(root, emit, opts); 9 | 10 | dp.close = noop; 11 | dp.updateInput = noop; 12 | dp.shouldFocusOnRender = opts.shouldFocusOnRender; 13 | 14 | dp.computeSelectedDate = function () { 15 | return opts.hilightedDate; 16 | }; 17 | 18 | dp.attachToDom = function () { 19 | var appendTo = opts.appendTo || root; 20 | appendTo.appendChild(dp.el); 21 | }; 22 | 23 | dp.open(); 24 | 25 | return dp; 26 | } 27 | -------------------------------------------------------------------------------- /src/views/day-picker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages the calendar / day-picker view. 3 | */ 4 | 5 | import {Key} from '../lib/dom'; 6 | import {now, datesEq, shiftMonth, shiftDay} from '../lib/date-manip'; 7 | 8 | export default { 9 | onKeyDown: keyDown, 10 | onClick: { 11 | 'dp-day': selectDay, 12 | 'dp-next': gotoNextMonth, 13 | 'dp-prev': gotoPrevMonth, 14 | 'dp-today': selectToday, 15 | 'dp-clear': clear, 16 | 'dp-close': close, 17 | 'dp-cal-month': showMonthPicker, 18 | 'dp-cal-year': showYearPicker, 19 | }, 20 | render: render 21 | }; 22 | 23 | /** 24 | * view renders the calendar (day picker) as an HTML string. 25 | * 26 | * @param {DatePickerContext} context the date picker being rendered 27 | * @returns {string} 28 | */ 29 | function render(dp) { 30 | var opts = dp.opts; 31 | var lang = opts.lang; 32 | var state = dp.state; 33 | var dayNames = lang.days; 34 | var dayOffset = opts.dayOffset || 0; 35 | var selectedDate = state.selectedDate; 36 | var hilightedDate = state.hilightedDate; 37 | var hilightedMonth = hilightedDate.getMonth(); 38 | var today = now().getTime(); 39 | 40 | return ( 41 | '
' + 42 | '
' + 43 | '' + 44 | '' + 47 | '' + 50 | '' + 51 | '
' + 52 | '
' + 53 | dayNames.map(function (name, i) { 54 | return ( 55 | '' + dayNames[(i + dayOffset) % dayNames.length] + '' 56 | ); 57 | }).join('') + 58 | mapDays(hilightedDate, dayOffset, function (date) { 59 | var isNotInMonth = date.getMonth() !== hilightedMonth; 60 | var isDisabled = !opts.inRange(date); 61 | var isToday = date.getTime() === today; 62 | var className = 'dp-day'; 63 | className += (isNotInMonth ? ' dp-edge-day' : ''); 64 | className += (datesEq(date, hilightedDate) ? ' dp-current' : ''); 65 | className += (datesEq(date, selectedDate) ? ' dp-selected' : ''); 66 | className += (isDisabled ? ' dp-day-disabled' : ''); 67 | className += (isToday ? ' dp-day-today' : ''); 68 | className += ' ' + opts.dateClass(date, dp); 69 | 70 | return ( 71 | '' 74 | ); 75 | }) + 76 | '
' + 77 | '
' + 78 | '' + 79 | '' + 80 | '' + 81 | '
' + 82 | '
' 83 | ); 84 | } 85 | 86 | /** 87 | * keyDown handles the key down event for the day-picker 88 | * 89 | * @param {Event} e 90 | * @param {DatePickerContext} dp 91 | */ 92 | function keyDown(e, dp) { 93 | var key = e.keyCode; 94 | var shiftBy = 95 | (key === Key.left) ? -1 : 96 | (key === Key.right) ? 1 : 97 | (key === Key.up) ? -7 : 98 | (key === Key.down) ? 7 : 99 | 0; 100 | 101 | if (key === Key.esc) { 102 | dp.close(); 103 | } else if (shiftBy) { 104 | e.preventDefault(); 105 | dp.setState({ 106 | hilightedDate: shiftDay(dp.state.hilightedDate, shiftBy) 107 | }); 108 | } 109 | } 110 | 111 | function selectToday(e, dp) { 112 | dp.setState({ 113 | selectedDate: now(), 114 | }); 115 | } 116 | 117 | function clear(e, dp) { 118 | dp.setState({ 119 | selectedDate: null, 120 | }); 121 | } 122 | 123 | function close(e, dp) { 124 | dp.close(); 125 | } 126 | 127 | function showMonthPicker(e, dp) { 128 | dp.setState({ 129 | view: 'month' 130 | }); 131 | } 132 | 133 | function showYearPicker(e, dp) { 134 | dp.setState({ 135 | view: 'year' 136 | }); 137 | } 138 | 139 | function gotoNextMonth(e, dp) { 140 | var hilightedDate = dp.state.hilightedDate; 141 | dp.setState({ 142 | hilightedDate: shiftMonth(hilightedDate, 1) 143 | }); 144 | } 145 | 146 | function gotoPrevMonth(e, dp) { 147 | var hilightedDate = dp.state.hilightedDate; 148 | dp.setState({ 149 | hilightedDate: shiftMonth(hilightedDate, -1) 150 | }); 151 | } 152 | 153 | function selectDay(e, dp) { 154 | dp.setState({ 155 | selectedDate: new Date(parseInt(e.target.getAttribute('data-date'))), 156 | }); 157 | } 158 | 159 | function mapDays(currentDate, dayOffset, fn) { 160 | var result = ''; 161 | var iter = new Date(currentDate); 162 | iter.setDate(1); 163 | iter.setDate(1 - iter.getDay() + dayOffset); 164 | 165 | // If we are showing monday as the 1st of the week, 166 | // and the monday is the 2nd of the month, the sunday won't 167 | // show, so we need to shift backwards 168 | if (dayOffset && iter.getDate() === dayOffset + 1) { 169 | iter.setDate(dayOffset - 6); 170 | } 171 | 172 | // We are going to have 6 weeks always displayed to keep a consistent 173 | // calendar size 174 | for (var day = 0; day < (6 * 7); ++day) { 175 | result += fn(iter); 176 | iter.setDate(iter.getDate() + 1); 177 | } 178 | 179 | return result; 180 | } 181 | -------------------------------------------------------------------------------- /src/views/month-picker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages the month-picker view. 3 | */ 4 | 5 | import {Key} from '../lib/dom'; 6 | import {shiftMonth, setMonth} from '../lib/date-manip'; 7 | 8 | export default { 9 | onKeyDown: keyDown, 10 | onClick: { 11 | 'dp-month': onChooseMonth 12 | }, 13 | render: render 14 | }; 15 | 16 | function onChooseMonth(e, dp) { 17 | dp.setState({ 18 | hilightedDate: setMonth(dp.state.hilightedDate, parseInt(e.target.getAttribute('data-month'))), 19 | view: 'day', 20 | }); 21 | } 22 | 23 | /** 24 | * render renders the month picker as an HTML string 25 | * 26 | * @param {DatePickerContext} dp the date picker context 27 | * @returns {string} 28 | */ 29 | function render(dp) { 30 | var opts = dp.opts; 31 | var lang = opts.lang; 32 | var months = lang.months; 33 | var currentDate = dp.state.hilightedDate; 34 | var currentMonth = currentDate.getMonth(); 35 | 36 | return ( 37 | '
' + 38 | months.map(function (month, i) { 39 | var className = 'dp-month'; 40 | className += (currentMonth === i ? ' dp-current' : ''); 41 | 42 | return ( 43 | '' 46 | ); 47 | }).join('') + 48 | '
' 49 | ); 50 | } 51 | 52 | /** 53 | * keyDown handles keydown events that occur in the month picker 54 | * 55 | * @param {Event} e 56 | * @param {DatePickerContext} dp 57 | */ 58 | function keyDown(e, dp) { 59 | var key = e.keyCode; 60 | var shiftBy = 61 | (key === Key.left) ? -1 : 62 | (key === Key.right) ? 1 : 63 | (key === Key.up) ? -3 : 64 | (key === Key.down) ? 3 : 65 | 0; 66 | 67 | if (key === Key.esc) { 68 | dp.setState({ 69 | view: 'day', 70 | }); 71 | } else if (shiftBy) { 72 | e.preventDefault(); 73 | dp.setState({ 74 | hilightedDate: shiftMonth(dp.state.hilightedDate, shiftBy, true) 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/views/year-picker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages the year-picker view. 3 | */ 4 | 5 | import {Key} from '../lib/dom'; 6 | import {setYear, shiftYear, constrainDate} from '../lib/date-manip'; 7 | 8 | export default { 9 | render: render, 10 | onKeyDown: keyDown, 11 | onClick: { 12 | 'dp-year': onChooseYear 13 | }, 14 | }; 15 | 16 | /** 17 | * view renders the year picker as an HTML string. 18 | * 19 | * @param {DatePickerContext} dp the date picker context 20 | * @returns {string} 21 | */ 22 | function render(dp) { 23 | var state = dp.state; 24 | var currentYear = state.hilightedDate.getFullYear(); 25 | var selectedYear = state.selectedDate.getFullYear(); 26 | 27 | return ( 28 | '
' + 29 | mapYears(dp, function (year) { 30 | var className = 'dp-year'; 31 | className += (year === currentYear ? ' dp-current' : ''); 32 | className += (year === selectedYear ? ' dp-selected' : ''); 33 | 34 | return ( 35 | '' 38 | ); 39 | }) + 40 | '
' 41 | ); 42 | } 43 | 44 | function onChooseYear(e, dp) { 45 | dp.setState({ 46 | hilightedDate: setYear(dp.state.hilightedDate, parseInt(e.target.getAttribute('data-year'))), 47 | view: 'day', 48 | }); 49 | } 50 | 51 | function keyDown(e, dp) { 52 | var key = e.keyCode; 53 | var opts = dp.opts; 54 | var shiftBy = 55 | (key === Key.left || key === Key.up) ? 1 : 56 | (key === Key.right || key === Key.down) ? -1 : 57 | 0; 58 | 59 | if (key === Key.esc) { 60 | dp.setState({ 61 | view: 'day', 62 | }); 63 | } else if (shiftBy) { 64 | e.preventDefault(); 65 | var shiftedYear = shiftYear(dp.state.hilightedDate, shiftBy); 66 | 67 | dp.setState({ 68 | hilightedDate: constrainDate(shiftedYear, opts.min, opts.max), 69 | }); 70 | } 71 | } 72 | 73 | function mapYears(dp, fn) { 74 | var result = ''; 75 | var max = dp.opts.max.getFullYear(); 76 | 77 | for (var i = max; i >= dp.opts.min.getFullYear(); --i) { 78 | result += fn(i); 79 | } 80 | 81 | return result; 82 | } 83 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "node": true, 5 | "es6": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/browser.test.js: -------------------------------------------------------------------------------- 1 | /* global expect, beforeAll, afterAll */ 2 | import webdriver, {By, until} from 'selenium-webdriver'; 3 | import chrome from 'selenium-webdriver/chrome' 4 | 5 | const options = new chrome.Options(); 6 | options.addArguments( 7 | // '--headless', 8 | // Use --disable-gpu to avoid an error from a missing Mesa library, as per 9 | // https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md 10 | '--disable-gpu'); 11 | 12 | /** 13 | * @type {webdriver.ThenableWebDriver} 14 | */ 15 | let driver; 16 | 17 | describe('browser', () => { 18 | beforeAll(() => { 19 | driver = new webdriver.Builder() 20 | .forBrowser('chrome') 21 | .setChromeOptions(options) 22 | .build(); 23 | }); 24 | 25 | afterAll(() => { 26 | driver.quit(); 27 | }); 28 | 29 | beforeEach(async () => { 30 | await driver.get('http://localhost:8080/test/'); 31 | }); 32 | 33 | describe('modal mode', () => { 34 | it('should show the modal on click', async () => { 35 | const el = await driver.findElement(By.css('.modal-txt')).click(); 36 | const current = await currentEl(driver, '.dp-modal'); 37 | 38 | await elDateIs(current, new Date()); 39 | }); 40 | 41 | it('sets focus back to the original input when shift + tab', async () => { 42 | const el = await driver.findElement(By.css('.modal-txt')).click(); 43 | const current = await currentEl(driver, '.dp-modal'); 44 | await current.sendKeys(webdriver.Key.SHIFT, webdriver.Key.TAB); 45 | await driver.wait(untilRemoved('.dp-modal')); 46 | const txtFocused = await driver.executeScript(function () { 47 | return document.activeElement.className; 48 | }); 49 | 50 | expect(txtFocused).toEqual('modal-txt'); 51 | }); 52 | 53 | it('sets focus back to the original input when tab', async () => { 54 | const el = await driver.findElement(By.css('.modal-txt')).click(); 55 | const current = await currentEl(driver, '.dp-modal'); 56 | await current.sendKeys(webdriver.Key.TAB); 57 | await driver.wait(untilRemoved('.dp-modal')); 58 | const focused = await driver.wait(() => driver.executeScript(function () { 59 | return document.activeElement.className === 'modal-txt'; 60 | })); 61 | 62 | expect(focused).toBeTruthy(); 63 | }); 64 | 65 | it('should hide the modal when the input re-gains focus', async () => { 66 | const el = await driver.findElement(By.css('.modal-txt')).click(); 67 | await driver.findElement(By.css('.dp-modal')); 68 | await driver.executeScript('document.querySelector(".dp-modal").focus();'); 69 | await driver.wait(untilRemoved('.dp-modal')); 70 | }); 71 | 72 | it('should hide the modal when close is clicked', async () => { 73 | const el = await driver.findElement(By.css('.modal-txt')).click(); 74 | await driver.findElement(By.css('.dp-close')).click(); 75 | await driver.wait(untilRemoved('.dp-modal')); 76 | }); 77 | 78 | it('should select today when today is clicked', async () => { 79 | const el = await driver.findElement(By.css('.modal-txt')).click(); 80 | await driver.findElement(byText('Today')).click(); 81 | await driver.wait(untilRemoved('.dp-modal')); 82 | const val = await driver.findElement(By.css('.modal-txt')).getAttribute('value'); 83 | const now = new Date(); 84 | 85 | expect(val).toEqual(`${now.getMonth() + 1}/${now.getDate()}/${now.getFullYear()}`); 86 | }); 87 | 88 | it('should change the date when a date is clicked', async () => { 89 | await driver.executeScript(` 90 | document.write(''); 91 | TinyDatePicker('.click-test'); 92 | `); 93 | const el = await driver.findElement(By.css('.click-test')).click(); 94 | const day = await driver.findElement(By.css('[data-date="1144468800000"]')); 95 | day.click(); 96 | await driver.wait(untilRemoved('.dp-modal')); 97 | const val = await driver.findElement(By.css('.click-test')).getAttribute('value'); 98 | 99 | expect(val).toEqual('4/8/2006'); 100 | }); 101 | 102 | it('should show the prev month when prev arrow is clicked', async () => { 103 | await driver.executeScript(` 104 | document.write(''); 105 | TinyDatePicker(document.querySelector('.my-modal')); 106 | `); 107 | 108 | const el = await driver.findElement(By.css('.my-modal')).click(); 109 | await driver.findElement(By.css('.dp-prev')).click(); 110 | const current = await currentEl(driver, '.dp-modal'); 111 | 112 | await elDateIs(current, '6/30/2017'); 113 | }); 114 | 115 | it('should show the next month when next arrow is clicked', async () => { 116 | await driver.executeScript(` 117 | document.write(''); 118 | TinyDatePicker(document.querySelector('.my-modal')); 119 | `); 120 | 121 | const el = await driver.findElement(By.css('.my-modal')).click(); 122 | await driver.findElement(By.css('.dp-next')).click(); 123 | const current = await currentEl(driver, '.dp-modal'); 124 | 125 | await elDateIs(current, '2/28/2018'); 126 | }); 127 | 128 | it('should clear the date field when clear is clicked', async () => { 129 | await driver.executeScript(` 130 | document.write(''); 131 | TinyDatePicker(document.querySelector('.my-modal')); 132 | `); 133 | const el = await driver.findElement(By.css('.my-modal')).click(); 134 | await driver.findElement(byText('Clear')).click(); 135 | await driver.wait(untilRemoved('.dp-modal')); 136 | const val = await driver.findElement(By.css('.my-modal')).getAttribute('value'); 137 | 138 | expect(val).toEqual(''); 139 | }); 140 | 141 | it('should emit open event', async () => { 142 | await driver.executeScript(` 143 | document.write(''); 144 | const el = document.querySelector('.my-modal'); 145 | TinyDatePicker(el).on({ 146 | open: () => window.pickerOpened = true, 147 | }); 148 | `); 149 | await driver.findElement(By.css('.my-modal')).click(); 150 | await currentEl(driver, '.dp-modal'); 151 | const emitted = await driver.executeScript(function () { 152 | return pickerOpened; 153 | }); 154 | 155 | expect(emitted).toBeTruthy(); 156 | }); 157 | 158 | it('should remove all handlers on destroy', async () => { 159 | await driver.executeScript(` 160 | document.write(''); 161 | window.myModal = TinyDatePicker(document.querySelector('.my-modal')); 162 | window.myModal.open(); 163 | `); 164 | 165 | await currentEl(driver, '.dp-modal'); 166 | await driver.executeScript('window.myModal.destroy();'); 167 | await driver.wait(untilRemoved('.dp-modal')); 168 | 169 | await driver.findElement(By.css('.my-modal')).click(); 170 | const modalVisible = await driver.executeScript(function () { 171 | return !!document.querySelector('.dp-modal'); 172 | }); 173 | 174 | expect(modalVisible).toBeFalsy(); 175 | }); 176 | 177 | it('should fire events', async () => { 178 | await driver.executeScript(` 179 | let count = 0; 180 | const events = {}; 181 | window.myModalEvents = events; 182 | document.write(''); 183 | window.myModal = TinyDatePicker(document.querySelector('.my-modal')); 184 | window.myModal.on({ 185 | open: () => events.open = ++count, 186 | close: () => events.close = ++count, 187 | statechange: () => events.statechange = ++count, 188 | select: () => events.select = ++count, 189 | }); 190 | `); 191 | 192 | await driver.findElement(By.css('.my-modal')).click(); 193 | await driver.findElement(byText('17')).click(); 194 | await driver.wait(untilRemoved('.dp-modal')); 195 | 196 | const myModalEvents = await driver.executeScript(function () { 197 | return window.myModalEvents; 198 | }); 199 | 200 | expect(myModalEvents).toEqual({ 201 | open: 1, 202 | close: 3, 203 | statechange: 4, 204 | select: 2 205 | }); 206 | }); 207 | 208 | it('should allow manual closing', async () => { 209 | await driver.executeScript(` 210 | document.write(''); 211 | window.myModal = TinyDatePicker(document.querySelector('.my-modal')); 212 | `); 213 | await driver.findElement(By.css('.my-modal')).click(); 214 | await currentEl(driver, '.dp-modal'); 215 | await driver.executeScript('myModal.close();'); 216 | driver.wait(untilRemoved('.dp-modal')); 217 | }); 218 | 219 | it('should allow manual opening', async () => { 220 | await driver.executeScript(` 221 | document.write(''); 222 | const myModal = TinyDatePicker(document.querySelector('.my-modal')); 223 | myModal.open(); 224 | `); 225 | await currentEl(driver, '.dp-modal'); 226 | const current = await currentEl(driver, '.dp-modal'); 227 | await elDateIs(current, '4/5/2006'); 228 | }); 229 | 230 | it('should show the modal input gains focus', async () => { 231 | await driver.executeScript(` 232 | const el = document.querySelector('.modal-txt'); 233 | el.value = '1/2/2018'; 234 | el.focus(); 235 | `); 236 | 237 | const current = await currentEl(driver, '.dp-modal'); 238 | 239 | await elDateIs(current, '1/2/2018'); 240 | }); 241 | 242 | it('allows manual state change', async () => { 243 | await driver.executeScript(` 244 | document.write(''); 245 | const myModal = TinyDatePicker(document.querySelector('.my-input')); 246 | myModal.open(); 247 | myModal.setState({ 248 | view: 'month', 249 | }); 250 | `); 251 | 252 | const monthPickerShowing = await driver.executeScript(function () { 253 | return !!document.querySelector('.dp-months'); 254 | }) 255 | expect(monthPickerShowing).toBeTruthy(); 256 | const month = await driver.findElement(byText('September')).getAttribute('data-month'); 257 | expect(month).toEqual('8'); 258 | }); 259 | 260 | it('shows day picker on open regardless of previous view state', async () => { 261 | await driver.executeScript(` 262 | document.write(''); 263 | const myModal = TinyDatePicker(document.querySelector('.my-input')); 264 | myModal.setState({ 265 | view: 'month', 266 | }); 267 | myModal.open(); 268 | `); 269 | 270 | const monthPickerShowing = await driver.executeScript(function () { 271 | return !!document.querySelector('.dp-months'); 272 | }) 273 | expect(monthPickerShowing).toBeFalsy(); 274 | }); 275 | 276 | it('allows month to be changed', async () => { 277 | await driver.executeScript(` 278 | const el = document.querySelector('.modal-txt'); 279 | el.value = '5/6/2017'; 280 | `); 281 | const el = await driver.findElement(By.css('.modal-txt')); 282 | await el.click(); 283 | await driver.findElement(byText('May')).click(); 284 | await driver.findElement(byText('February')).click(); 285 | 286 | const current = await currentEl(driver, '.dp-modal'); 287 | await elDateIs(current, '2/6/2017'); 288 | }); 289 | 290 | it('allows year to be changed', async () => { 291 | await driver.executeScript(` 292 | const el = document.querySelector('.modal-txt'); 293 | el.value = '5/6/2017'; 294 | `); 295 | const el = await driver.findElement(By.css('.modal-txt')); 296 | await el.click(); 297 | await driver.findElement(byText('2017')).click(); 298 | await driver.findElement(byText('2013')).click(); 299 | 300 | const current = await currentEl(driver, '.dp-modal'); 301 | await elDateIs(current, '5/6/2013'); 302 | }); 303 | }); 304 | 305 | describe('dropdown mode', () => { 306 | it('should show the dropdown below the input', async () => { 307 | const el = await driver.findElement(By.css('.non-modal-txt')); 308 | await el.click(); 309 | await currentEl(driver, '.dp-below'); 310 | await el.sendKeys('10/11/2017'); 311 | const current = await currentEl(driver, '.dp-below'); 312 | 313 | await elDateIs(current, '10/11/2017'); 314 | }); 315 | 316 | it('should keep focus on the input', async () => { 317 | const el = await driver.findElement(By.css('.non-modal-txt')); 318 | await el.click(); 319 | await currentEl(driver, '.dp-below'); 320 | const focused = await driver.executeScript(function () { 321 | return document.activeElement.className; 322 | }); 323 | 324 | expect(focused).toContain('non-modal-txt'); 325 | }); 326 | 327 | it('shows days of the week, starting with Sunday', async () => { 328 | const el = await driver.findElement(By.css('.non-modal-txt')); 329 | await el.click(); 330 | const current = await currentEl(driver, '.dp-below'); 331 | const dayCols = await driver.findElements(By.css('.dp-col-header')); 332 | const days = await Promise.all(dayCols.map(el => el.getText().then(s => s.toUpperCase()))); 333 | 334 | expect(days).toEqual(['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']); 335 | 336 | // Verify that the first day is a Sunday 337 | await driver.findElement(By.css('.dp-day')).click(); 338 | 339 | const val = await driver.findElement(By.css('.non-modal-txt')).getAttribute('value'); 340 | 341 | expect(new Date(val).getDay()).toEqual(0); 342 | }); 343 | 344 | it('shows days of the week, starting with Monday', async () => { 345 | await driver.executeScript(` 346 | document.write(''); 347 | TinyDatePicker(document.querySelector('.my-input'), { 348 | mode: 'dp-below', 349 | dayOffset: 1, 350 | }); 351 | `); 352 | const el = await driver.findElement(By.css('.my-input')); 353 | await el.click(); 354 | const current = await currentEl(driver, '.dp-below'); 355 | const dayCols = await driver.findElements(By.css('.dp-col-header')); 356 | const days = await Promise.all(dayCols.map(el => el.getText().then(s => s.toUpperCase()))); 357 | 358 | expect(days).toEqual(['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']); 359 | 360 | // Verify that the first day is a Monday 361 | await driver.findElement(By.css('.dp-day')).click(); 362 | 363 | const val = await driver.findElement(By.css('.my-input')).getAttribute('value'); 364 | 365 | expect(new Date(val).getDay()).toEqual(1); 366 | }); 367 | 368 | it('handles case when a visible monday is the 2nd of the week', async () => { 369 | await driver.executeScript(` 370 | document.write(''); 371 | TinyDatePicker(document.querySelector('.my-input'), { 372 | mode: 'dp-below', 373 | dayOffset: 1, 374 | min: '1/1/1900', 375 | max: '1/1/2020', 376 | }); 377 | `); 378 | const el = await driver.findElement(By.css('.my-input')); 379 | await el.click(); 380 | const txt = await driver.findElement(By.css('.dp-day')).getText(); 381 | expect(txt).toEqual('26'); 382 | }); 383 | 384 | it('allows custom parsing and formatting', async () => { 385 | await driver.executeScript(` 386 | document.write(''); 387 | TinyDatePicker(document.querySelector('.my-input'), { 388 | mode: 'dp-below', 389 | parse: (s) => new Date(s.replace('IT IS ', '').replace('!', '')), 390 | format: (dt) => 'IT IS ' + dt.toDateString() + '!', 391 | }); 392 | `); 393 | 394 | const el = await driver.findElement(By.css('.my-input')).click(); 395 | 396 | const current = await currentEl(driver, '.dp-below'); 397 | 398 | await elDateIs(current, '1/2/2050'); 399 | 400 | await driver.findElement(byText('Today')).click(); 401 | const val = await driver.findElement(By.css('.my-input')).getAttribute('value'); 402 | 403 | expect(val).toEqual('IT IS ' + new Date().toDateString() + '!'); 404 | }); 405 | 406 | it('does not preselect if date already filled', async () => { 407 | await driver.executeScript(` 408 | document.write(''); 409 | TinyDatePicker(document.querySelector('.my-input'), { 410 | mode: 'dp-below', 411 | hilightedDate: '2/3/2005', 412 | }); 413 | `); 414 | 415 | await driver.findElement(By.css('.my-input')).click(); 416 | const current = await currentEl(driver, '.dp-below'); 417 | 418 | await elDateIs(current, '1/2/2050'); 419 | }); 420 | }); 421 | 422 | describe('permanent mode', () => { 423 | it('constrains date to min if today is less than max', async () => { 424 | await driver.executeScript(` 425 | document.write('
'); 426 | TinyDatePicker(document.querySelector('.perm'), { 427 | mode: 'dp-permanent', 428 | min: '1/1/2200', 429 | max: '1/1/2310', 430 | }); 431 | `); 432 | const current = await driver.findElement(By.css('.dp-current')); 433 | 434 | await elDateIs(current, '1/1/2200'); 435 | }); 436 | 437 | it('constrains date to max if today is greater than max', async () => { 438 | await driver.executeScript(` 439 | document.write('
'); 440 | TinyDatePicker(document.querySelector('.perm'), { 441 | mode: 'dp-permanent', 442 | min: '1/1/2000', 443 | max: '1/1/2010', 444 | }); 445 | `); 446 | const current = await driver.findElement(By.css('.dp-current')); 447 | 448 | await elDateIs(current, '1/1/2010'); 449 | }); 450 | 451 | it('disallows selection of disabled date', async () => { 452 | await driver.executeScript(` 453 | document.write('
'); 454 | TinyDatePicker(document.querySelector('.perm'), { 455 | mode: 'dp-permanent', 456 | min: '1/1/2000', 457 | max: '1/1/2010', 458 | }); 459 | `); 460 | 461 | await driver.findElement(byText('11')).click(); 462 | let current = await currentEl(driver, '.dp-permanent'); 463 | await elDateIs(current, '1/1/2010'); 464 | 465 | await driver.findElement(byText('28')).click(); 466 | current = await currentEl(driver, '.dp-permanent'); 467 | await elDateIs(current, '12/28/2009'); 468 | }); 469 | 470 | it('hilights the hilightedDate date', async () => { 471 | await driver.executeScript(` 472 | document.write('
'); 473 | TinyDatePicker(document.querySelector('.perm'), { 474 | mode: 'dp-permanent', 475 | hilightedDate: '2/3/2005', 476 | min: '1/1/2000', 477 | max: '1/1/2010', 478 | }); 479 | `); 480 | 481 | const current = await currentEl(driver, '.dp-permanent'); 482 | 483 | await elDateIs(current, '2/3/2005'); 484 | }); 485 | 486 | it('allows custom formatting', async () => { 487 | await driver.executeScript(` 488 | document.write('
'); 489 | TinyDatePicker(document.querySelector('.perm'), { 490 | mode: 'dp-permanent', 491 | lang: { 492 | close: 'Oseclay', 493 | today: 'Oodaytay', 494 | clear: 'Earclay', 495 | days: ['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF', 'GGG'], 496 | }, 497 | }); 498 | `); 499 | 500 | const close = await driver.findElement(By.css('.dp-cal-footer .dp-close')).getText(); 501 | const today = await driver.findElement(By.css('.dp-cal-footer .dp-today')).getText(); 502 | const clear = await driver.findElement(By.css('.dp-cal-footer .dp-clear')).getText(); 503 | const dayCols = await driver.findElements(By.css('.dp-col-header')); 504 | const days = await Promise.all(dayCols.map(el => el.getText().then(s => s.toUpperCase()))); 505 | 506 | expect(close).toEqual('Oseclay'); 507 | expect(today).toEqual('Oodaytay'); 508 | expect(clear).toEqual('Earclay'); 509 | expect(days).toEqual(['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF', 'GGG']); 510 | }); 511 | 512 | it('allows customizing the css class for any given date', async () => { 513 | await driver.executeScript(` 514 | document.write('
'); 515 | TinyDatePicker(document.querySelector('.perm'), { 516 | mode: 'dp-permanent', 517 | dateClass: (dt) => 'hoi-' + dt.getDate(), 518 | }); 519 | `); 520 | 521 | const txt = await driver.wait(until.elementLocated(By.css('.hoi-8'))).getText(); 522 | 523 | expect(txt).toEqual('8'); 524 | }); 525 | 526 | it('allows customizing which dates are disabled', async () => { 527 | await driver.executeScript(` 528 | document.write('
'); 529 | TinyDatePicker(document.querySelector('.perm'), { 530 | mode: 'dp-permanent', 531 | inRange: (dt) => dt.getDate() !== 17, 532 | }); 533 | `); 534 | 535 | const txt = await driver.wait(until.elementLocated(By.css('.dp-day-disabled'))).getText(); 536 | 537 | expect(txt).toEqual('17'); 538 | }); 539 | }); 540 | }); 541 | 542 | /** 543 | * elDateIs checks that the specified element represents the specified date 544 | * 545 | * @param {webdriver.WebElement} el 546 | * @param {Date|string} dt 547 | */ 548 | async function elDateIs(el, dt) { 549 | const txt = await el.getText(); 550 | const dataDate = await el.getAttribute('data-date'); 551 | const actualDate = new Date(parseInt(dataDate, 10)); 552 | const expectedDate = new Date(dt); 553 | 554 | expect(actualDate.toDateString()).toEqual(expectedDate.toDateString()); 555 | expect(txt).toEqual(expectedDate.getDate().toString()); 556 | } 557 | 558 | /** 559 | * currentEl selects the currently selected date element for the specified calendar 560 | * 561 | * @param {webdriver.ThenableWebDriver} driver 562 | * @param {string} calendarSelector 563 | * @return {webdriver.WebElement} 564 | */ 565 | async function currentEl(driver, calendarSelector) { 566 | return await driver.wait(until.elementLocated(By.css(calendarSelector + ' .dp-current'))); 567 | } 568 | 569 | /** 570 | * untilRemoved can be used in a wait to wait until the specified element is removed 571 | * from the DOM 572 | * 573 | * @param {string} selector 574 | * @returns {Promise} 575 | */ 576 | async function untilRemoved(selector) { 577 | return () => driver.findElement(selector).then(() => false).catch(() => true); 578 | } 579 | 580 | function byText(txt) { 581 | return By.xpath(`//*[contains(translate(normalize-space(text()), ' ', ''), '${txt}')]`); 582 | } 583 | -------------------------------------------------------------------------------- /test/date-manip.test.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | import { 3 | now, 4 | datesEq, 5 | shiftDay, 6 | shiftMonth, 7 | shiftYear, 8 | setYear, 9 | setMonth, 10 | dateOrParse, 11 | constrainDate 12 | } from '../src/lib/date-manip'; 13 | 14 | describe('date-manip', () => { 15 | const sToDt = (s) => new Date(s); 16 | 17 | describe('now', () => { 18 | it('does not have time', () => { 19 | const dt = now(); 20 | expect(dt.getHours()).toEqual(0); 21 | expect(dt.getMinutes()).toEqual(0); 22 | expect(dt.getSeconds()).toEqual(0); 23 | expect(dt.getMilliseconds()).toEqual(0); 24 | }); 25 | 26 | it('is today', () => { 27 | expect(now().toDateString()).toEqual(new Date().toDateString()); 28 | }); 29 | }); 30 | 31 | describe('datesEq', () => { 32 | it('does not care about time', () => { 33 | expect(datesEq(now(), new Date())).toBeTruthy(); 34 | }); 35 | 36 | it('handles nulls', () => { 37 | expect(datesEq()).toBeTruthy(); 38 | expect(datesEq(now())).toBeFalsy(); 39 | }); 40 | }); 41 | 42 | describe('shiftDay', () => { 43 | it('does not mutate the date', () => { 44 | const dt = now(); 45 | const tomorrow = shiftDay(dt, 1); 46 | 47 | expect(datesEq(dt, tomorrow)).toBeFalsy(); 48 | }); 49 | 50 | it('shifts forward in time', () => { 51 | const dt = sToDt('1/2/2010'); 52 | const tomorrow = shiftDay(dt, 1); 53 | expect(tomorrow).toEqual(sToDt('1/3/2010')); 54 | }); 55 | 56 | it('shifts backward in time', () => { 57 | const dt = sToDt('1/2/2010'); 58 | const tenDaysAgo = shiftDay(dt, -10); 59 | expect(tenDaysAgo).toEqual(sToDt('12/23/2009')); 60 | }); 61 | }); 62 | 63 | describe('shiftMonth', () => { 64 | it('does not mutate the date', () => { 65 | const dt = now(); 66 | const nextMonth = shiftMonth(dt, 1); 67 | 68 | expect(dt).not.toEqual(nextMonth); 69 | }); 70 | 71 | it('shifts forward', () => { 72 | const dt = sToDt('1/2/1999'); 73 | const nextMonth = shiftMonth(dt, 2); 74 | 75 | expect(nextMonth).toEqual(sToDt('3/2/1999')); 76 | }); 77 | 78 | it('shifts backward past year', () => { 79 | const dt = sToDt('1/2/1999'); 80 | const nextMonth = shiftMonth(dt, -3); 81 | 82 | expect(nextMonth).toEqual(sToDt('10/2/1998')); 83 | }); 84 | 85 | it('does not shift year if told not to', () => { 86 | const dt = sToDt('1/2/1999'); 87 | const nextMonth = shiftMonth(dt, -3, true); 88 | 89 | expect(nextMonth).toEqual(sToDt('10/2/1999')); 90 | }); 91 | 92 | it('handles month end 31 to 30', () => { 93 | const dt = sToDt('5/31/2017'); 94 | const nextMonth = shiftMonth(dt, 1); 95 | 96 | expect(nextMonth).toEqual(sToDt('6/30/2017')); 97 | }); 98 | 99 | it('handles month end back from 31 to 28', () => { 100 | const dt = sToDt('3/31/2018'); 101 | const nextMonth = shiftMonth(dt, -1); 102 | 103 | expect(nextMonth).toEqual(sToDt('2/28/2018')); 104 | }); 105 | 106 | it('handles month end back from 31 to 30', () => { 107 | const dt = sToDt('5/31/2018'); 108 | const nextMonth = shiftMonth(dt, -1); 109 | 110 | expect(nextMonth).toEqual(sToDt('4/30/2018')); 111 | }); 112 | }); 113 | 114 | describe('shiftYear', () => { 115 | it('does not mutate the date', () => { 116 | const dt = now(); 117 | const tomorrow = shiftYear(dt, 1); 118 | 119 | expect(datesEq(dt, tomorrow)).toBeFalsy(); 120 | }); 121 | 122 | it('shifts forward in time', () => { 123 | const dt = sToDt('3/4/2005'); 124 | const next = shiftYear(dt, 1); 125 | expect(next).toEqual(sToDt('3/4/2006')); 126 | }); 127 | 128 | it('shifts backward in time', () => { 129 | const dt = sToDt('3/4/2005'); 130 | const next = shiftYear(dt, -5); 131 | expect(next).toEqual(sToDt('3/4/2000')); 132 | }); 133 | }); 134 | 135 | describe('setYear', () => { 136 | it('does not mutate the date', () => { 137 | const dt = now(); 138 | const next = setYear(dt, 2010); 139 | 140 | expect(datesEq(dt, next)).toBeFalsy(); 141 | }); 142 | 143 | it('sets the year', () => { 144 | const dt = sToDt('3/4/2005'); 145 | const next = setYear(dt, 2011); 146 | expect(next).toEqual(sToDt('3/4/2011')); 147 | }); 148 | }); 149 | 150 | describe('setMonth', () => { 151 | it('does not mutate the date', () => { 152 | const dt = now(); 153 | const next = setMonth(dt, (dt.getMonth() + 1) % 12); 154 | 155 | expect(datesEq(dt, next)).toBeFalsy(); 156 | }); 157 | 158 | it('sets the month', () => { 159 | const dt = sToDt('3/4/2005'); 160 | const next = setMonth(dt, 8); // zero-based 161 | expect(next).toEqual(sToDt('9/4/2005')); 162 | }); 163 | 164 | it('handles month end 31 to 30', () => { 165 | const dt = sToDt('5/31/2017'); 166 | const nextMonth = setMonth(dt, 5); 167 | 168 | expect(nextMonth).toEqual(sToDt('6/30/2017')); 169 | }); 170 | 171 | it('handles month end back from 31 to 28', () => { 172 | const dt = sToDt('3/31/2018'); 173 | const nextMonth = setMonth(dt, 1); 174 | 175 | expect(nextMonth).toEqual(sToDt('2/28/2018')); 176 | }); 177 | 178 | it('handles month end back from 31 to 30', () => { 179 | const dt = sToDt('5/31/2018'); 180 | const nextMonth = setMonth(dt, 3); 181 | 182 | expect(nextMonth).toEqual(sToDt('4/30/2018')); 183 | }); 184 | }); 185 | 186 | describe('dateOrParse', () => { 187 | it('handles dates', () => { 188 | const toDt = dateOrParse(sToDt); 189 | const dt = now(); 190 | 191 | expect(dt).toEqual(toDt(dt)); 192 | }); 193 | 194 | it('handles strings', () => { 195 | const toDt = dateOrParse(sToDt); 196 | const dt = sToDt('1/2/2003'); 197 | 198 | expect(dt).toEqual(toDt('1/2/2003')); 199 | }); 200 | }); 201 | 202 | describe('constrainDate', () => { 203 | it('does not change the date if in range', () => { 204 | const dt = sToDt('1/2/2003'); 205 | const min = sToDt('1/1/2003'); 206 | const max = sToDt('1/1/2020'); 207 | 208 | expect(dt).toEqual(constrainDate(dt, min, max)); 209 | }); 210 | 211 | it('gives min if dt is too small', () => { 212 | const dt = sToDt('1/2/2003'); 213 | const min = sToDt('2/1/2003'); 214 | const max = sToDt('1/1/2020'); 215 | 216 | expect(min).toEqual(constrainDate(dt, min, max)); 217 | }); 218 | 219 | it('gives max if dt is too big', () => { 220 | const dt = sToDt('1/2/2033'); 221 | const min = sToDt('2/1/2003'); 222 | const max = sToDt('1/1/2020'); 223 | 224 | expect(max).toEqual(constrainDate(dt, min, max)); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /test/date-picker-options.test.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | import DatePickerOptions from '../src/date-picker-options'; 3 | 4 | describe('DatePickerOptions', () => { 5 | it('lang defaults to english', () => { 6 | const opts = DatePickerOptions(); 7 | expect(opts.lang.close).toEqual('Close'); 8 | expect(opts.lang.days).toEqual( 9 | ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 10 | ); 11 | expect(opts.lang.months).toEqual([ 12 | 'January', 13 | 'February', 14 | 'March', 15 | 'April', 16 | 'May', 17 | 'June', 18 | 'July', 19 | 'August', 20 | 'September', 21 | 'October', 22 | 'November', 23 | 'December', 24 | ]); 25 | }); 26 | 27 | it('allows overriding some lang props', () => { 28 | const opts = DatePickerOptions({ 29 | lang: {close: 'X'} 30 | }); 31 | expect(opts.lang.close).toEqual('X'); 32 | expect(opts.lang.today).toEqual('Today'); 33 | expect(opts.lang.clear).toEqual('Clear'); 34 | }); 35 | 36 | it('defaults min/max to roughly 100 years', () => { 37 | const opts = DatePickerOptions(); 38 | const dt = new Date(); 39 | 40 | expect(opts.min.getFullYear()).toBeLessThan(dt.getFullYear() - 99); 41 | expect(opts.min.getFullYear()).toBeGreaterThan(dt.getFullYear() - 101); 42 | 43 | expect(opts.max.getFullYear()).toBeGreaterThan(dt.getFullYear() + 99); 44 | expect(opts.max.getFullYear()).toBeLessThan(dt.getFullYear() + 101); 45 | }); 46 | 47 | it('includes min/max in custom inRange function', () => { 48 | const opts = DatePickerOptions({ 49 | min: '10/20/2000', 50 | max: '10/20/2010', 51 | inRange: (d) => d.getFullYear() !== 2001, 52 | }); 53 | 54 | expect(opts.inRange(new Date('10/20/2000'))).toBeTruthy(); 55 | expect(opts.inRange(new Date('10/20/2010'))).toBeTruthy(); 56 | expect(opts.inRange(new Date('10/21/2010'))).toBeFalsy(); 57 | expect(opts.inRange(new Date('10/19/2000'))).toBeFalsy(); 58 | expect(opts.inRange(new Date('10/19/2001'))).toBeFalsy(); 59 | expect(opts.inRange(new Date('10/19/2002'))).toBeTruthy(); 60 | }); 61 | 62 | it('formats date w/ American english style by default', () => { 63 | const opts = DatePickerOptions(); 64 | 65 | expect(opts.format(new Date('2017-09-07T22:44:23.163Z'))) 66 | .toEqual('9/7/2017'); 67 | }); 68 | 69 | it('parses dates using the default date constructor', () => { 70 | const opts = DatePickerOptions(); 71 | 72 | expect(opts.parse('9/7/2017').getFullYear()).toEqual(2017); 73 | expect(opts.parse('10/2/2017').getDate()).toEqual(2); 74 | expect(opts.parse('11/2/2017').getMonth()).toEqual(10); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/date-range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tiny Date Picker Demo 6 | 7 | 8 | 9 | 10 | 11 | 106 | 107 | 108 | 109 | 110 |
111 | 114 |
115 |
116 |

117 | Date Range Picker Demo 118 |

119 | 120 | - 121 | 122 |
123 |
124 | 198 |
199 |
200 | 201 | 202 | -------------------------------------------------------------------------------- /test/emitter.test.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | import Emitter from '../src/lib/emitter'; 3 | 4 | describe('Emitter', () => { 5 | it('emits events', () => { 6 | const emitter = Emitter(); 7 | let count = 0; 8 | emitter.on('hi', (_, x) => count = x); 9 | emitter.emit('hi', 42); 10 | expect(count).toEqual(42); 11 | }); 12 | 13 | it('can have multiple handlers', () => { 14 | const emitter = Emitter(); 15 | const words = []; 16 | emitter.on('yo', (_, s) => words[0] = s); 17 | emitter.on('yo', (_, s) => words[1] = s); 18 | emitter.emit('yo', 'hoi'); 19 | expect(words).toEqual(['hoi', 'hoi']); 20 | }); 21 | 22 | it('can take a hash table of handlers', () => { 23 | const emitter = Emitter(); 24 | let one = 0; 25 | let two = 0; 26 | 27 | emitter.on({ 28 | one: () => ++one, 29 | two: () => ++two, 30 | }); 31 | 32 | emitter.emit('one'); 33 | emitter.emit('two'); 34 | emitter.emit('two'); 35 | 36 | expect(one).toEqual(1); 37 | expect(two).toEqual(2); 38 | }); 39 | 40 | it('can unregister a single handler', () => { 41 | const emitter = Emitter(); 42 | const words = []; 43 | const h1 = (_, s) => words[0] = s; 44 | const h2 = (_, s) => words[1] = s; 45 | emitter.on('yo', h1); 46 | emitter.on('yo', h2); 47 | emitter.off('yo', h1); 48 | emitter.emit('yo', 'hoi'); 49 | expect(words).toEqual([undefined, 'hoi']); 50 | }); 51 | 52 | it('can unregister all handlers of a certain event', () => { 53 | const emitter = Emitter(); 54 | let yo = 0; 55 | let hi = 0; 56 | 57 | emitter.on('yo', () => ++yo); 58 | emitter.on('yo', () => ++yo); 59 | emitter.on('hi', () => ++hi); 60 | 61 | emitter.emit('yo'); 62 | emitter.emit('hi'); 63 | 64 | expect(yo).toEqual(2); 65 | expect(hi).toEqual(1); 66 | 67 | emitter.off('yo'); 68 | 69 | emitter.emit('yo'); 70 | emitter.emit('hi'); 71 | 72 | expect(yo).toEqual(2); 73 | expect(hi).toEqual(2); 74 | }); 75 | 76 | it('can unregister all handlers of all events', () => { 77 | const emitter = Emitter(); 78 | let yo = 0; 79 | let hi = 0; 80 | 81 | emitter.on('yo', () => ++yo); 82 | emitter.on('yo', () => ++yo); 83 | emitter.on('hi', () => ++hi); 84 | 85 | emitter.emit('yo'); 86 | emitter.emit('hi'); 87 | 88 | expect(yo).toEqual(2); 89 | expect(hi).toEqual(1); 90 | 91 | emitter.off(); 92 | 93 | emitter.emit('yo'); 94 | emitter.emit('hi'); 95 | 96 | expect(yo).toEqual(2); 97 | expect(hi).toEqual(1); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/fns.test.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | import {bufferFn} from '../src/lib/fns'; 3 | 4 | describe('bufferFn', () => { 5 | it('only runs once within a window of time', () => { 6 | return new Promise((resolve, reject) => { 7 | let count = 0; 8 | const f = bufferFn(1, () => ++count); 9 | 10 | f(); 11 | f(); 12 | f(); 13 | 14 | expect(count).toEqual(0); 15 | 16 | setTimeout(() => { 17 | try { 18 | expect(count).toEqual(1); 19 | resolve(); 20 | } catch (err) { 21 | reject(err); 22 | } 23 | }, 5); 24 | }); 25 | }); 26 | 27 | it('only runs twice if called outside of the window', () => { 28 | return new Promise((resolve, reject) => { 29 | let count = 0; 30 | const f = bufferFn(1, () => ++count); 31 | 32 | f(); 33 | f(); 34 | f(); 35 | 36 | expect(count).toEqual(0); 37 | 38 | setTimeout(() => { 39 | f(); 40 | f(); 41 | f(); 42 | }, 10); 43 | 44 | setTimeout(() => { 45 | try { 46 | expect(count).toEqual(2); 47 | resolve(); 48 | } catch (err) { 49 | reject(err); 50 | } 51 | }, 15); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tiny Date Picker Demo 6 | 7 | 8 | 9 | 10 | 114 | 115 | 116 |
117 |

Tiny Date Picker

118 |
119 | 120 | 121 |
122 |
123 | 124 | 125 |
126 | 129 |
130 | 131 | 132 | 153 | 154 | -------------------------------------------------------------------------------- /tiny-date-picker.css: -------------------------------------------------------------------------------- 1 | .dp-modal { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background: rgba(255, 255, 255, 0.75); 8 | } 9 | 10 | .dp { 11 | position: relative; 12 | background: #FFF; 13 | box-shadow: 2px 2px 16px rgba(0, 0, 0, 0.25); 14 | line-height: 1.4; 15 | border-radius: 4px; 16 | max-height: 400px; 17 | z-index: 1000; 18 | padding-top: 6px; 19 | overflow: hidden; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | .dp:before { 24 | content: ' '; 25 | height: 6px; 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | background: #3B99FC; 31 | background: linear-gradient(-90deg, #3B99FC 0%, #8AEFC8 100%); 32 | } 33 | 34 | .dp-permanent .dp { 35 | padding-top: 0; 36 | border: 1px solid #EEE; 37 | box-shadow: none; 38 | } 39 | 40 | .dp-permanent .dp:before { 41 | display: none; 42 | } 43 | 44 | .dp-cal { 45 | min-height: 300px; 46 | } 47 | 48 | .dp-below { 49 | position: absolute; 50 | font-size: 0.8em; 51 | width: 400px; 52 | max-width: 100vw; 53 | } 54 | 55 | .dp-permanent { 56 | position: relative; 57 | font-size: 0.8em; 58 | width: 400px; 59 | max-width: 100vw; 60 | } 61 | 62 | .dp-permanent .dp{ 63 | z-index: 0; 64 | } 65 | 66 | .dp-modal .dp { 67 | position: absolute; 68 | top: 50%; 69 | left: 50%; 70 | max-width: 600px; 71 | width: calc(100% - 4em); 72 | transform: translate(-50%, -50%); 73 | animation: slide-up 0.3s forwards; 74 | } 75 | 76 | .dp-months { 77 | padding: 24px; 78 | } 79 | 80 | .dp-years { 81 | box-sizing: border-box; 82 | max-height: 400px; 83 | padding: 8px 0; 84 | overflow: auto !important; /* HACK for Chrome on Android */ 85 | } 86 | 87 | .dp-cal-month, 88 | .dp-cal-year, 89 | .dp-day, 90 | .dp-month, 91 | .dp-year { 92 | box-sizing: border-box; 93 | text-align: center; 94 | text-decoration: none; 95 | position: relative; 96 | color: #3B404D; 97 | border-radius: 2px; 98 | border: 0; 99 | background: transparent; 100 | } 101 | 102 | .dp-cal-header { 103 | position: relative; 104 | text-align: center; 105 | padding-bottom: 16px; 106 | background: #f5f5f5; 107 | } 108 | 109 | .dp-next, 110 | .dp-prev { 111 | position: absolute; 112 | width: 30px; 113 | height: 30px; 114 | overflow: hidden; 115 | top: 14px; 116 | color: #777; 117 | border-radius: 2px; 118 | border: 0; 119 | background: transparent; 120 | } 121 | 122 | .dp-next:focus, 123 | .dp-prev:focus, 124 | .dp-next:hover, 125 | .dp-prev:hover { 126 | outline: none; 127 | color: inherit; 128 | } 129 | 130 | .dp-prev { 131 | left: 24px; 132 | } 133 | 134 | .dp-next { 135 | right: 24px; 136 | } 137 | 138 | .dp-prev:before, 139 | .dp-next:before { 140 | content: ''; 141 | border: 2px solid; 142 | width: 10px; 143 | height: 10px; 144 | display: inline-block; 145 | transform: rotate(-45deg); 146 | transition: border-color 0.2s; 147 | margin: 9px 0 40px 4px; 148 | } 149 | 150 | .dp-prev:before { 151 | border-right: 0; 152 | border-bottom: 0; 153 | } 154 | 155 | .dp-next:before { 156 | border-left: 0; 157 | border-top: 0; 158 | margin-left: 0; 159 | margin-right: 4px; 160 | } 161 | 162 | .dp-cal-month, 163 | .dp-cal-year { 164 | display: inline-block; 165 | font-size: 1.4em; 166 | padding: 16px 8px 8px; 167 | outline: none; 168 | } 169 | 170 | .dp-cal-footer { 171 | text-align: center; 172 | background: #f5f5f5; 173 | } 174 | 175 | .dp-day-today:after { 176 | content: ''; 177 | height: 0; 178 | width: 0; 179 | border: 7px solid #227BD7; 180 | border-bottom-color: transparent; 181 | border-left-color: transparent; 182 | position: absolute; 183 | top: 0; 184 | right: 0; 185 | } 186 | 187 | .dp-close, 188 | .dp-clear, 189 | .dp-today { 190 | box-sizing: border-box; 191 | display: inline-block; 192 | width: 33%; 193 | padding: 8px; 194 | text-decoration: none; 195 | color: inherit; 196 | border: 0; 197 | background: transparent; 198 | } 199 | 200 | .dp-permanent .dp-close, 201 | .dp-permanent .dp-clear { 202 | display: none; 203 | } 204 | 205 | .dp-close:active, 206 | .dp-clear:active, 207 | .dp-today:active, 208 | .dp-next:active, 209 | .dp-prev:active, 210 | .dp-cal-month:active, 211 | .dp-cal-year:active { 212 | background: #75BCFC; 213 | color: white; 214 | } 215 | 216 | @media screen and (min-device-width: 1200px) { 217 | .dp-close:hover, 218 | .dp-close:focus, 219 | .dp-clear:hover, 220 | .dp-clear:focus, 221 | .dp-today:hover, 222 | .dp-today:focus, 223 | .dp-next:hover, 224 | .dp-next:focus, 225 | .dp-prev:hover, 226 | .dp-prev:focus, 227 | .dp-cal-month:focus, 228 | .dp-cal-month:hover, 229 | .dp-cal-year:hover, 230 | .dp-cal-year:focus { 231 | background: #75BCFC; 232 | color: white; 233 | } 234 | } 235 | 236 | .dp-col-header, 237 | .dp-day { 238 | width: 14.28571429%; 239 | display: inline-block; 240 | padding: 8px; 241 | text-align: center; 242 | } 243 | 244 | .dp-col-header { 245 | color: #AAA; 246 | text-transform: uppercase; 247 | font-weight: 300; 248 | font-size: 0.8em; 249 | padding: 8px 0; 250 | } 251 | 252 | .dp-month { 253 | width: 33%; 254 | display: inline-block; 255 | padding: 8px; 256 | } 257 | 258 | .dp-year { 259 | display: block; 260 | padding: 8px 40px; 261 | width: 100%; 262 | } 263 | 264 | .dp-edge-day { 265 | color: #AAA; 266 | } 267 | 268 | .dp-day:hover, 269 | .dp-month:hover, 270 | .dp-year:hover, 271 | .dp-current:focus, 272 | .dp-current, 273 | .dp-day:focus, 274 | .dp-month:focus, 275 | .dp-year:focus { 276 | outline: none; 277 | background: #75BCFC; 278 | color: white; 279 | } 280 | 281 | .dp-selected:hover, 282 | .dp-selected:focus, 283 | .dp-selected { 284 | background: #3B99FC; 285 | color: #FFF; 286 | } 287 | 288 | .dp-day-disabled { 289 | background: transparent; 290 | color: #DDD; 291 | } 292 | 293 | .dp-day-disabled:focus, 294 | .dp-day-disabled:hover { 295 | background: #DDD; 296 | } 297 | 298 | .dp-focuser { 299 | position: absolute; 300 | z-index: 0; 301 | top: 50%; 302 | left: 50%; 303 | } 304 | 305 | /* Responsive overrides */ 306 | @media (max-width: 480px), (max-height: 480px) { 307 | .dp-modal .dp { 308 | font-size: 0.9em; 309 | width: auto; 310 | width: 100%; 311 | } 312 | 313 | .dp-day-of-week, 314 | .dp-day { 315 | padding: 8px; 316 | } 317 | } 318 | 319 | @keyframes slide-up { 320 | 0% { 321 | transform: translate(-50%, 100%); 322 | } 323 | 100% { 324 | transform: translate(-50%, -50%); 325 | } 326 | } -------------------------------------------------------------------------------- /tiny-date-picker.min.css: -------------------------------------------------------------------------------- 1 | .dp-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.75)}.dp{position:relative;background:#FFF;box-shadow:2px 2px 16px rgba(0,0,0,0.25);line-height:1.4;border-radius:4px;max-height:400px;z-index:1000;padding-top:6px;overflow:hidden;-webkit-tap-highlight-color:transparent}.dp:before{content:' ';height:6px;position:absolute;top:0;left:0;right:0;background:#3b99fc;background:linear-gradient(-90deg,#3b99fc 0,#8aefc8 100%)}.dp-permanent .dp{padding-top:0;border:1px solid #EEE;box-shadow:none}.dp-permanent .dp:before{display:none}.dp-cal{min-height:300px}.dp-below{position:absolute;font-size:.8em;width:400px;max-width:100vw}.dp-permanent{position:relative;font-size:.8em;width:400px;max-width:100vw}.dp-permanent .dp{z-index:0}.dp-modal .dp{position:absolute;top:50%;left:50%;max-width:600px;width:calc(100% - 4em);transform:translate(-50%,-50%);animation:slide-up .3s forwards}.dp-months{padding:24px}.dp-years{box-sizing:border-box;max-height:400px;padding:8px 0;overflow:auto !important}.dp-cal-month,.dp-cal-year,.dp-day,.dp-month,.dp-year{box-sizing:border-box;text-align:center;text-decoration:none;position:relative;color:#3b404d;border-radius:2px;border:0;background:transparent}.dp-cal-header{position:relative;text-align:center;padding-bottom:16px;background:#f5f5f5}.dp-next,.dp-prev{position:absolute;width:30px;height:30px;overflow:hidden;top:14px;color:#777;border-radius:2px;border:0;background:transparent}.dp-next:focus,.dp-prev:focus,.dp-next:hover,.dp-prev:hover{outline:0;color:inherit}.dp-prev{left:24px}.dp-next{right:24px}.dp-prev:before,.dp-next:before{content:'';border:2px solid;width:10px;height:10px;display:inline-block;transform:rotate(-45deg);transition:border-color .2s;margin:9px 0 40px 4px}.dp-prev:before{border-right:0;border-bottom:0}.dp-next:before{border-left:0;border-top:0;margin-left:0;margin-right:4px}.dp-cal-month,.dp-cal-year{display:inline-block;font-size:1.4em;padding:16px 8px 8px;outline:0}.dp-cal-footer{text-align:center;background:#f5f5f5}.dp-day-today:after{content:'';height:0;width:0;border:7px solid #227bd7;border-bottom-color:transparent;border-left-color:transparent;position:absolute;top:0;right:0}.dp-close,.dp-clear,.dp-today{box-sizing:border-box;display:inline-block;width:33%;padding:8px;text-decoration:none;color:inherit;border:0;background:transparent}.dp-permanent .dp-close,.dp-permanent .dp-clear{display:none}.dp-close:active,.dp-clear:active,.dp-today:active,.dp-next:active,.dp-prev:active,.dp-cal-month:active,.dp-cal-year:active{background:#75bcfc;color:white}@media screen and (min-device-width:1200px){.dp-close:hover,.dp-close:focus,.dp-clear:hover,.dp-clear:focus,.dp-today:hover,.dp-today:focus,.dp-next:hover,.dp-next:focus,.dp-prev:hover,.dp-prev:focus,.dp-cal-month:focus,.dp-cal-month:hover,.dp-cal-year:hover,.dp-cal-year:focus{background:#75bcfc;color:white}}.dp-col-header,.dp-day{width:14.28571429%;display:inline-block;padding:8px;text-align:center}.dp-col-header{color:#AAA;text-transform:uppercase;font-weight:300;font-size:.8em;padding:8px 0}.dp-month{width:33%;display:inline-block;padding:8px}.dp-year{display:block;padding:8px 40px;width:100%}.dp-edge-day{color:#AAA}.dp-day:hover,.dp-month:hover,.dp-year:hover,.dp-current:focus,.dp-current,.dp-day:focus,.dp-month:focus,.dp-year:focus{outline:0;background:#75bcfc;color:white}.dp-selected:hover,.dp-selected:focus,.dp-selected{background:#3b99fc;color:#FFF}.dp-day-disabled{background:transparent;color:#DDD}.dp-day-disabled:focus,.dp-day-disabled:hover{background:#DDD}.dp-focuser{position:absolute;z-index:0;top:50%;left:50%}@media(max-width:480px),(max-height:480px){.dp-modal .dp{font-size:.9em;width:auto;width:100%}.dp-day-of-week,.dp-day{padding:8px}}@keyframes slide-up{0%{transform:translate(-50%,100%)}100%{transform:translate(-50%,-50%)}} 2 | --------------------------------------------------------------------------------