├── .gitignore ├── index.js ├── lib ├── utils.js ├── template.html ├── calendar.css ├── dayrange.js ├── calendar.js └── days.js ├── .jshintrc ├── Makefile ├── History.md ├── component.json ├── package.json ├── test └── dayrange.js ├── Readme.md └── example.html /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/calendar'); -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Clamp `month`. 4 | * 5 | * @param {Number} month 6 | * @return {Number} 7 | * @api public 8 | */ 9 | 10 | exports.clamp = function(month){ 11 | if (month > 11) return 0; 12 | if (month < 0) return 11; 13 | return month; 14 | }; 15 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "laxbreak": true, 5 | "laxcomma": true, 6 | "supernew": true, 7 | "globals": { 8 | "console": false, 9 | "describe": false, 10 | "exports": false, 11 | "it": false, 12 | "module": false, 13 | "require": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
-------------------------------------------------------------------------------- /lib/calendar.css: -------------------------------------------------------------------------------- 1 | 2 | .calendar-table { 3 | border: 1px solid #eee; 4 | padding: 5px; 5 | margin: 5px; 6 | } 7 | 8 | .calendar-table .prev-day, 9 | .calendar-table .next-day { 10 | color: #999; 11 | } 12 | 13 | .calendar-table td { 14 | text-align: center; 15 | } 16 | 17 | .calendar-table td { 18 | user-select: none; 19 | } 20 | 21 | .calendar-table td a { 22 | cursor: pointer; 23 | } 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = index.js \ 2 | lib/calendar.js \ 3 | lib/days.js 4 | 5 | all: lint test build 6 | 7 | build: $(SRC) lib/template.html lib/calendar.css | components 8 | @component build --dev 9 | 10 | components: 11 | @component install --dev 12 | 13 | clean: 14 | rm -fr build components 15 | 16 | lint: 17 | @./node_modules/.bin/jshint $(SRC) 18 | 19 | test: 20 | @./node_modules/.bin/mocha \ 21 | --reporter spec 22 | 23 | .PHONY: clean test lint all 24 | -------------------------------------------------------------------------------- /lib/dayrange.js: -------------------------------------------------------------------------------- 1 | var Bounds = require('bounds'); 2 | 3 | var type; 4 | 5 | if (typeof window !== 'undefined') { 6 | type = require('type'); 7 | } else { 8 | type = require('type-component'); 9 | } 10 | 11 | module.exports = DayRange; 12 | 13 | function date(d) { 14 | return type(d) === 'date' ? d : new Date(d[0], d[1], d[2]); 15 | } 16 | 17 | function DayRange(min, max) { 18 | return this.min(min).max(max); 19 | } 20 | 21 | Bounds(DayRange.prototype); 22 | 23 | DayRange.prototype._compare = function(a, b) { 24 | return date(a).getTime() - date(b).getTime(); 25 | } 26 | 27 | DayRange.prototype._distance = function(a, b) { 28 | return Math.abs(this.compare(a, b)); 29 | } 30 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.2.0 / 2015-04-27 3 | ================== 4 | 5 | * enable changing locales for calendar 6 | * use component templates to translate HTML 7 | 8 | 0.1.0 / 2014-04-02 9 | ================== 10 | 11 | * add dates restriction: min/max functions 12 | 13 | 0.0.5 / 2013-06-06 14 | ================== 15 | 16 | * fix tags 17 | 18 | 0.0.4 / 2013-05-31 19 | ================== 20 | 21 | * pin deps 22 | * fix short months selection on 31th of the month 23 | 24 | 0.0.3 / 2012-09-21 25 | ================== 26 | 27 | * fix start of the month date padding [colinf] 28 | 29 | 0.0.2 / 2012-09-20 30 | ================== 31 | 32 | * add package.json 33 | * fix prev/next month day selection [colinf] 34 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calendar", 3 | "repo": "component/calendar", 4 | "description": "Calendar component", 5 | "version": "0.2.0", 6 | "keywords": [ 7 | "calendar", 8 | "date", 9 | "ui" 10 | ], 11 | "dependencies": { 12 | "code42day/bounds": "*", 13 | "component/bind": "*", 14 | "component/domify": "*", 15 | "component/classes": "*", 16 | "component/event": "*", 17 | "component/map": "*", 18 | "component/range": "1.0.0", 19 | "component/emitter": "1.0.0", 20 | "component/in-groups-of": "1.0.0", 21 | "component/type": "*", 22 | "yields/empty": "*", 23 | "stephenmathieson/normalize": "*" 24 | }, 25 | "development": { 26 | "component/aurora-calendar": "*" 27 | }, 28 | "styles": [ 29 | "lib/calendar.css" 30 | ], 31 | "scripts": [ 32 | "index.js", 33 | "lib/utils.js", 34 | "lib/calendar.js", 35 | "lib/dayrange.js", 36 | "lib/days.js" 37 | ], 38 | "templates": [ 39 | "lib/template.html" 40 | ], 41 | "demo": "http://component.github.io/calendar/", 42 | "license": "MIT" 43 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calendar-component", 3 | "description": "Calendar component", 4 | "version": "0.2.0", 5 | "keywords": [ 6 | "calendar", 7 | "date", 8 | "ui" 9 | ], 10 | "dependencies": { 11 | "domify": "1.0.0", 12 | "emitter-component": "1.0.1", 13 | "classes-component": "1.1.3", 14 | "range-component": "1.0.0", 15 | "in-groups-of": "component/in-groups-of#1.0.0", 16 | "event-component": "~0.1.0" 17 | }, 18 | "browser": { 19 | "event": "event-component", 20 | "emitter": "emitter-component", 21 | "classes": "classes-component", 22 | "range": "range-component" 23 | }, 24 | "component": { 25 | "styles": [ 26 | "lib/calendar.css" 27 | ], 28 | "scripts": { 29 | "calendar/index.js": "index.js", 30 | "calendar/lib/utils.js": "lib/utils.js", 31 | "calendar/lib/template.js": "lib/template.js", 32 | "calendar/lib/calendar.js": "lib/calendar.js", 33 | "calendar/lib/days.js": "lib/days.js" 34 | } 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/component/calendar.git" 39 | }, 40 | "devDependencies": { 41 | "bounds": "~1.0.0", 42 | "jshint": "^2.7.0", 43 | "mocha": "~1", 44 | "type-component": "0.0.1" 45 | } 46 | } -------------------------------------------------------------------------------- /test/dayrange.js: -------------------------------------------------------------------------------- 1 | var DayRange = require('../lib/dayrange') 2 | , assert = require('assert'); 3 | 4 | describe('day range', function(){ 5 | it('should consider all dates as valid if no min/max specified', function(){ 6 | var dr = new DayRange; 7 | assert.ok(!dr.before(new Date)); 8 | assert.ok(!dr.after(new Date)); 9 | assert.ok(dr.valid([2002, 12, 10])); 10 | }); 11 | 12 | it('should consider dates inside of the range as valid', function(){ 13 | var dr = new DayRange([2014, 3, 2], [2014, 4, 3]); 14 | assert.ok(dr.before([2014, 3, 1])); 15 | assert.ok(!dr.valid([2014, 3, 1])); 16 | assert.ok(dr.valid([2014, 3, 2])); 17 | assert.ok(dr.valid([2014, 3, 30])); 18 | assert.ok(dr.valid([2014, 4, 3])); 19 | assert.ok(!dr.valid([2014, 4, 4])); 20 | assert.ok(dr.after([2014, 4, 4])); 21 | }); 22 | 23 | it('should work with mixture of dates and arrays', function(){ 24 | var dr = new DayRange() 25 | .min([2014, 3, 2]) 26 | .max(new Date(2014, 4, 3)); 27 | assert.ok(dr.before(new Date(2014, 3, 1))); 28 | assert.ok(!dr.valid(new Date(2014, 3, 1))); 29 | assert.ok(dr.valid(new Date(2014, 3, 2))); 30 | assert.ok(dr.valid(new Date(2014, 3, 30))); 31 | assert.ok(dr.valid(new Date(2014, 4, 3))); 32 | assert.ok(!dr.valid(new Date(2014, 4, 4))); 33 | assert.ok(dr.after(new Date(2014, 4, 4))); 34 | }); 35 | 36 | it('should work if only min is specified', function(){ 37 | var dr = new DayRange([2013, 3, 3]); 38 | assert.ok(!dr.valid([2013, 3, 2])); 39 | assert.ok(dr.valid([2013, 3, 3])); 40 | assert.ok(dr.valid([2013, 3, 4])); 41 | }); 42 | 43 | it('should work if only max is specified', function(){ 44 | var dr = new DayRange(null, [2013, 3, 3]); 45 | assert.ok(dr.valid([2013, 3, 2])); 46 | assert.ok(dr.valid([2013, 3, 3])); 47 | assert.ok(!dr.valid([2013, 3, 4])); 48 | }); 49 | }); -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Calendar 3 | 4 | Calendar UI component designed for use as a date-picker, 5 | full-sized calendar or anything in-between. 6 | 7 | ![javascript calendar component](http://f.cl.ly/items/2u3w1D421W0C370Z3G1U/Screen%20Shot%202012-10-11%20at%2014.32.41.png) 8 | 9 | Live demo is [here](http://component.github.io/calendar/) 10 | 11 | ## Installation 12 | 13 | $ component install component/calendar 14 | 15 | ## Example 16 | 17 | ```js 18 | var Calendar = require('calendar'); 19 | var cal = new Calendar; 20 | cal.el.appendTo('body'); 21 | ``` 22 | 23 | ## Events 24 | 25 | - `view change` (date, action) when the viewed month/year is changed without modification of the selected date. This can be done either by next/prev buttons or dropdown menu. The action will be "prev", "next", "month" or "year" depending on what action caused the view to change. 26 | - `change` (date) when the selected date is modified 27 | 28 | ## API 29 | 30 | ### new Calendar(date) 31 | 32 | Initialize a new `Calendar` with the given `date` shown, 33 | defaulting to now. 34 | 35 | ### Calendar#select(date) 36 | 37 | Select the given `date` (`Date` object). 38 | 39 | ### Calendar#show(date) 40 | 41 | Show the given `date`. This does _not_ select the given date, 42 | it simply ensures that it is visible in the current view. 43 | 44 | ### Calendar#showMonthSelect() 45 | 46 | Add month selection input. 47 | 48 | ### Calendar#showYearSelect([from], [to]) 49 | 50 | Add year selection input, with optional range specified by `from` and `to`, 51 | which default to the current year -10 / +10. 52 | 53 | ### Calendar#prev() 54 | 55 | Show the previous view (month). 56 | 57 | ### Calendar#next() 58 | 59 | Show the next view (month). 60 | 61 | ### Calendar#min() 62 | 63 | Define earliest valid date - calendar won't generate `change` events for dates before this one. 64 | 65 | ### Calendar#max() 66 | 67 | Define latest valid date - calendar won't generate `change` events for dates after this one. 68 | 69 | ### Calendar#locale({months, weekdaysMin}) 70 | 71 | Set alternative locale: 72 | - `months` - an array of 12 strings representing month names _January..December_. 73 | - `weekdaysMin` - an array of 7 strings representing day names shortcuts _Sunday..Saturday_ 74 | 75 | ## Themes 76 | 77 | [Aurora](https://github.com/component/aurora-calendar): 78 | 79 | ![](http://f.cl.ly/items/043N1r0e1L130y162R2f/Screen%20Shot%202012-09-17%20at%209.17.32%20PM.png) 80 | 81 | # License 82 | 83 | MIT 84 | 85 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Calendar 5 | 6 | 7 | 53 | 54 | 55 |

Calendar

56 |
57 |
58 | 59 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /lib/calendar.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var bind = require('bind') 7 | , domify = require('domify') 8 | , Emitter = require('emitter') 9 | , classes = require('classes') 10 | , Days = require('./days'); 11 | 12 | /** 13 | * Expose `Calendar`. 14 | */ 15 | 16 | module.exports = Calendar; 17 | 18 | /** 19 | * Initialize a new `Calendar` 20 | * with the given `date` defaulting 21 | * to now. 22 | * 23 | * Events: 24 | * 25 | * - `prev` when the prev link is clicked 26 | * - `next` when the next link is clicked 27 | * - `change` (date) when the selected date is modified 28 | * 29 | * @params {Date} date 30 | * @api public 31 | */ 32 | 33 | function Calendar(date) { 34 | if (!(this instanceof Calendar)) { 35 | return new Calendar(date); 36 | } 37 | 38 | Emitter.call(this); 39 | var self = this; 40 | this.el = domify('
'); 41 | this.days = new Days; 42 | this.el.appendChild(this.days.el); 43 | this.on('change', bind(this, this.show)); 44 | this.days.on('prev', bind(this, this.prev)); 45 | this.days.on('next', bind(this, this.next)); 46 | this.days.on('year', bind(this, this.menuChange, 'year')); 47 | this.days.on('month', bind(this, this.menuChange, 'month')); 48 | this.show(date || new Date); 49 | this.days.on('change', function(date){ 50 | self.emit('change', date); 51 | }); 52 | } 53 | 54 | /** 55 | * Mixin emitter. 56 | */ 57 | 58 | Emitter(Calendar.prototype); 59 | 60 | /** 61 | * Add class `name` to differentiate this 62 | * specific calendar for styling purposes, 63 | * for example `calendar.addClass('date-picker')`. 64 | * 65 | * @param {String} name 66 | * @return {Calendar} 67 | * @api public 68 | */ 69 | 70 | Calendar.prototype.addClass = function(name){ 71 | classes(this.el).add(name); 72 | return this; 73 | }; 74 | 75 | /** 76 | * Select `date`. 77 | * 78 | * @param {Date} date 79 | * @return {Calendar} 80 | * @api public 81 | */ 82 | 83 | Calendar.prototype.select = function(date){ 84 | if (this.days.validRange.valid(date)) { 85 | this.selected = date; 86 | this.days.select(date); 87 | } 88 | this.show(date); 89 | return this; 90 | }; 91 | 92 | /** 93 | * Show `date`. 94 | * 95 | * @param {Date} date 96 | * @return {Calendar} 97 | * @api public 98 | */ 99 | 100 | Calendar.prototype.show = function(date){ 101 | this._date = date; 102 | this.days.show(date); 103 | return this; 104 | }; 105 | 106 | /** 107 | * Set minimum valid date (inclusive) 108 | * 109 | * @param {Date} date 110 | * @api public 111 | */ 112 | 113 | Calendar.prototype.min = function(date) { 114 | this.days.validRange.min(date); 115 | return this; 116 | }; 117 | 118 | 119 | /** 120 | * Set maximum valid date (inclusive) 121 | * 122 | * @param {Date} date 123 | * @api public 124 | */ 125 | 126 | Calendar.prototype.max = function(date) { 127 | this.days.validRange.max(date); 128 | return this; 129 | }; 130 | 131 | /** 132 | * Enable a year dropdown. 133 | * 134 | * @param {Number} from 135 | * @param {Number} to 136 | * @return {Calendar} 137 | * @api public 138 | */ 139 | 140 | Calendar.prototype.showYearSelect = function(from, to){ 141 | from = from || this._date.getFullYear() - 10; 142 | to = to || this._date.getFullYear() + 10; 143 | this.days.yearMenu(from, to); 144 | this.show(this._date); 145 | return this; 146 | }; 147 | 148 | /** 149 | * Enable a month dropdown. 150 | * 151 | * @return {Calendar} 152 | * @api public 153 | */ 154 | 155 | Calendar.prototype.showMonthSelect = function(){ 156 | this.days.monthMenu(); 157 | this.show(this._date); 158 | return this; 159 | }; 160 | 161 | /** 162 | * Return the previous month. 163 | * 164 | * @return {Date} 165 | * @api private 166 | */ 167 | 168 | Calendar.prototype.prevMonth = function(){ 169 | var date = new Date(this._date); 170 | date.setDate(1); 171 | date.setMonth(date.getMonth() - 1); 172 | return date; 173 | }; 174 | 175 | /** 176 | * Return the next month. 177 | * 178 | * @return {Date} 179 | * @api private 180 | */ 181 | 182 | Calendar.prototype.nextMonth = function(){ 183 | var date = new Date(this._date); 184 | date.setDate(1); 185 | date.setMonth(date.getMonth() + 1); 186 | return date; 187 | }; 188 | 189 | /** 190 | * Show the prev view. 191 | * 192 | * @return {Calendar} 193 | * @api public 194 | */ 195 | 196 | Calendar.prototype.prev = function(){ 197 | this.show(this.prevMonth()); 198 | this.emit('view change', this.days.selectedMonth(), 'prev'); 199 | return this; 200 | }; 201 | 202 | /** 203 | * Show the next view. 204 | * 205 | * @return {Calendar} 206 | * @api public 207 | */ 208 | 209 | Calendar.prototype.next = function(){ 210 | this.show(this.nextMonth()); 211 | this.emit('view change', this.days.selectedMonth(), 'next'); 212 | return this; 213 | }; 214 | 215 | /** 216 | * Switch to the year or month selected by dropdown menu. 217 | * 218 | * @return {Calendar} 219 | * @api public 220 | */ 221 | 222 | Calendar.prototype.menuChange = function(action){ 223 | var date = this.days.selectedMonth(); 224 | this.show(date); 225 | this.emit('view change', date, action); 226 | return this; 227 | }; 228 | 229 | /** 230 | * Select locale 231 | * 232 | * @param {months, weekdaysMin} - array of months labels and array of weekdays shortcuts 233 | * @api public 234 | */ 235 | 236 | Calendar.prototype.locale = function(locale) { 237 | this.days.setLocale(locale); 238 | this.days.show(this._date); 239 | return this; 240 | }; 241 | -------------------------------------------------------------------------------- /lib/days.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var domify = require('domify') 7 | , Emitter = require('emitter') 8 | , classes = require('classes') 9 | , events = require('event') 10 | , map = require('map') 11 | , template = require('./template.html') 12 | , inGroupsOf = require('in-groups-of') 13 | , clamp = require('./utils').clamp 14 | , range = require('range') 15 | , empty = require('empty') 16 | , normalize = require('normalize') 17 | , DayRange = require('./dayrange'); 18 | 19 | /** 20 | * Default locale - en 21 | */ 22 | 23 | var LOCALE = { 24 | months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), 25 | weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_') 26 | }; 27 | 28 | 29 | /** 30 | * Get days in `month` for `year`. 31 | * 32 | * @param {Number} month 33 | * @param {Number} year 34 | * @return {Number} 35 | * @api private 36 | */ 37 | 38 | function daysInMonth(month, year) { 39 | return [31, (isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 40 | } 41 | 42 | /** 43 | * Check if `year` is a leap year. 44 | * 45 | * @param {Number} year 46 | * @return {Boolean} 47 | * @api private 48 | */ 49 | 50 | function isLeapYear(year) { 51 | return (0 === year % 400) 52 | || ((0 === year % 4) && (0 !== year % 100)) 53 | || (0 === year); 54 | } 55 | 56 | 57 | /** 58 | * Expose `Days`. 59 | */ 60 | 61 | module.exports = Days; 62 | 63 | /** 64 | * Initialize a new `Days` view. 65 | * 66 | * Emits: 67 | * 68 | * - `prev` when prev link is clicked 69 | * - `next` when next link is clicked 70 | * - `change` (date) when a date is selected 71 | * 72 | * @api public 73 | */ 74 | 75 | function Days() { 76 | Emitter.call(this); 77 | var self = this; 78 | this.el = domify(template); 79 | classes(this.el).add('calendar-days'); 80 | this.head = this.el.tHead; 81 | this.body = this.el.tBodies[0]; 82 | this.title = this.head.querySelector('.title'); 83 | this.select(new Date); 84 | this.validRange = new DayRange; 85 | this.locale = LOCALE; 86 | 87 | // emit "day" 88 | events.bind(this.body, 'click', normalize(function(e){ 89 | if (e.target.tagName !== 'A') { 90 | return true; 91 | } 92 | 93 | e.preventDefault(); 94 | 95 | var el = e.target; 96 | var data = el.getAttribute('data-date').split('-'); 97 | if (!self.validRange.valid(data)) { 98 | return false; 99 | } 100 | var year = data[0]; 101 | var month = data[1]; 102 | var day = data[2]; 103 | var date = new Date(year, month, day); 104 | self.select(date); 105 | self.emit('change', date); 106 | return false; 107 | })); 108 | 109 | // emit "prev" 110 | events.bind(this.el.querySelector('.prev'), 'click', normalize(function(ev){ 111 | ev.preventDefault(); 112 | 113 | self.emit('prev'); 114 | return false; 115 | })); 116 | 117 | // emit "next" 118 | events.bind(this.el.querySelector('.next'), 'click', normalize(function(ev){ 119 | ev.preventDefault(); 120 | 121 | self.emit('next'); 122 | return false; 123 | })); 124 | } 125 | 126 | /** 127 | * Mixin emitter. 128 | */ 129 | 130 | Emitter(Days.prototype); 131 | 132 | /** 133 | * Select the given `date`. 134 | * 135 | * @param {Date} date 136 | * @return {Days} 137 | * @api public 138 | */ 139 | 140 | Days.prototype.select = function(date){ 141 | this.selected = date; 142 | return this; 143 | }; 144 | 145 | 146 | /** 147 | * Select locale 148 | * 149 | * @param {months, weekdaysMin} - array of months labels and array of weekdays shortcuts 150 | * @api public 151 | */ 152 | 153 | Days.prototype.setLocale = function(locale) { 154 | this.locale = locale; 155 | return this; 156 | }; 157 | 158 | /** 159 | * Show date selection. 160 | * 161 | * @param {Date} date 162 | * @api public 163 | */ 164 | 165 | Days.prototype.show = function(date){ 166 | var year = date.getFullYear(); 167 | var month = date.getMonth(); 168 | this.showSelectedYear(year); 169 | this.showSelectedMonth(month); 170 | var subhead = this.head.querySelector('.subheading'); 171 | if (subhead) { 172 | subhead.parentElement.removeChild(subhead); 173 | } 174 | 175 | this.head.appendChild(this.renderHeading(this.locale.weekdaysMin)); 176 | empty(this.body); 177 | this.body.appendChild(this.renderDays(date)); 178 | }; 179 | 180 | /** 181 | * Enable a year dropdown. 182 | * 183 | * @param {Number} from 184 | * @param {Number} to 185 | * @api public 186 | */ 187 | 188 | Days.prototype.yearMenu = function(from, to){ 189 | this.selectYear = true; 190 | this.title.querySelector('.year').innerHTML = yearDropdown(from, to); 191 | var self = this; 192 | events.bind(this.title.querySelector('.year .calendar-select'), 'change', function(){ 193 | self.emit('year'); 194 | return false; 195 | }); 196 | }; 197 | 198 | /** 199 | * Enable a month dropdown. 200 | * 201 | * @api public 202 | */ 203 | 204 | Days.prototype.monthMenu = function(){ 205 | this.selectMonth = true; 206 | this.title.querySelector('.month').innerHTML = monthDropdown(this.locale.months); 207 | var self = this; 208 | events.bind(this.title.querySelector('.month .calendar-select'), 'change', function(){ 209 | self.emit('month'); 210 | return false; 211 | }); 212 | }; 213 | 214 | /** 215 | * Return current year of view from title. 216 | * 217 | * @api private 218 | */ 219 | 220 | Days.prototype.titleYear = function(){ 221 | if (this.selectYear) { 222 | return this.title.querySelector('.year .calendar-select').value; 223 | } else { 224 | return this.title.querySelector('.year').innerHTML; 225 | } 226 | }; 227 | 228 | /** 229 | * Return current month of view from title. 230 | * 231 | * @api private 232 | */ 233 | 234 | Days.prototype.titleMonth = function(){ 235 | if (this.selectMonth) { 236 | return this.title.querySelector('.month .calendar-select').value; 237 | } else { 238 | return this.title.querySelector('.month').innerHTML; 239 | } 240 | }; 241 | 242 | /** 243 | * Return a date based on the field-selected month. 244 | * 245 | * @api public 246 | */ 247 | 248 | Days.prototype.selectedMonth = function(){ 249 | return new Date(this.titleYear(), this.titleMonth(), 1); 250 | }; 251 | 252 | /** 253 | * Render days of the week heading with 254 | * the given `length`, for example 2 for "Tu", 255 | * 3 for "Tue" etc. 256 | * 257 | * @param {String} len 258 | * @return {Element} 259 | * @api private 260 | */ 261 | 262 | Days.prototype.renderHeading = function(days){ 263 | var rows = '' + map(days, function(day){ 264 | return '' + day + ''; 265 | }).join('') + ''; 266 | return domify(rows); 267 | }; 268 | 269 | /** 270 | * Render days for `date`. 271 | * 272 | * @param {Date} date 273 | * @return {Element} 274 | * @api private 275 | */ 276 | 277 | Days.prototype.renderDays = function(date){ 278 | var rows = this.rowsFor(date); 279 | var html = map(rows, function(row){ 280 | return '' + row.join('') + ''; 281 | }).join('\n'); 282 | return domify(html); 283 | }; 284 | 285 | /** 286 | * Return rows array for `date`. 287 | * 288 | * This method calculates the "overflow" 289 | * from the previous month and into 290 | * the next in order to display an 291 | * even 5 rows. 292 | * 293 | * @param {Date} date 294 | * @return {Array} 295 | * @api private 296 | */ 297 | 298 | Days.prototype.rowsFor = function(date){ 299 | var selected = this.selected; 300 | var selectedDay = selected.getDate(); 301 | var selectedMonth = selected.getMonth(); 302 | var selectedYear = selected.getFullYear(); 303 | var month = date.getMonth(); 304 | var year = date.getFullYear(); 305 | 306 | // calculate overflow 307 | var start = new Date(date); 308 | start.setDate(1); 309 | var before = start.getDay(); 310 | var total = daysInMonth(month, year); 311 | var perRow = 7; 312 | var totalShown = perRow * Math.ceil((total + before) / perRow); 313 | var after = totalShown - (total + before); 314 | var cells = []; 315 | 316 | // cells before 317 | cells = cells.concat(cellsBefore(before, month, year, this.validRange)); 318 | 319 | // current cells 320 | for (var i = 0; i < total; ++i) { 321 | var day = i + 1 322 | , select = (day == selectedDay && month == selectedMonth && year == selectedYear); 323 | cells.push(renderDay([year, month, day], this.validRange, select)); 324 | } 325 | 326 | // after cells 327 | cells = cells.concat(cellsAfter(after, month, year, this.validRange)); 328 | 329 | return inGroupsOf(cells, 7); 330 | }; 331 | 332 | /** 333 | * Update view title or select input for `year`. 334 | * 335 | * @param {Number} year 336 | * @api private 337 | */ 338 | 339 | Days.prototype.showSelectedYear = function(year){ 340 | if (this.selectYear) { 341 | this.title.querySelector('.year .calendar-select').value = year; 342 | } else { 343 | this.title.querySelector('.year').innerHTML = year; 344 | } 345 | }; 346 | 347 | /** 348 | * Update view title or select input for `month`. 349 | * 350 | * @param {Number} month 351 | * @api private 352 | */ 353 | 354 | Days.prototype.showSelectedMonth = function(month) { 355 | if (this.selectMonth) { 356 | this.title.querySelector('.month .calendar-select').value = month; 357 | } else { 358 | this.title.querySelector('.month').innerHTML = this.locale.months[month]; 359 | } 360 | }; 361 | 362 | /** 363 | * Return `n` days before `month`. 364 | * 365 | * @param {Number} n 366 | * @param {Number} month 367 | * @return {Array} 368 | * @api private 369 | */ 370 | 371 | function cellsBefore(n, month, year, validRange){ 372 | var cells = []; 373 | if (month === 0) --year; 374 | var prev = clamp(month - 1); 375 | var before = daysInMonth(prev, year); 376 | while (n--) cells.push(renderDay([year, prev, before--], validRange, false, 'prev-day')); 377 | return cells.reverse(); 378 | } 379 | 380 | /** 381 | * Return `n` days after `month`. 382 | * 383 | * @param {Number} n 384 | * @param {Number} month 385 | * @return {Array} 386 | * @api private 387 | */ 388 | 389 | function cellsAfter(n, month, year, validRange){ 390 | var cells = []; 391 | var day = 0; 392 | if (month == 11) ++year; 393 | var next = clamp(month + 1); 394 | while (n--) cells.push(renderDay([year, next, ++day], validRange, false, 'next-day')); 395 | return cells; 396 | } 397 | 398 | 399 | /** 400 | * Day template. 401 | */ 402 | 403 | function renderDay(ymd, validRange, selected, style) { 404 | var date = 'data-date=' + ymd.join('-') 405 | , styles = [] 406 | , tdClass = '' 407 | , aClass = ''; 408 | 409 | if (selected) { 410 | tdClass = ' class="selected"'; 411 | } 412 | if (style) { 413 | styles.push(style); 414 | } 415 | if (!validRange.valid(ymd)) { 416 | styles.push('invalid'); 417 | } 418 | if (styles.length) { 419 | aClass = ' class="' + styles.join(' ') + '"'; 420 | } 421 | 422 | 423 | return '' + ymd[2] + ''; 424 | } 425 | 426 | /** 427 | * Year dropdown template. 428 | */ 429 | 430 | function yearDropdown(from, to) { 431 | var years = range(from, to, 'inclusive'); 432 | var options = map(years, yearOption).join(''); 433 | return ''; 434 | } 435 | 436 | /** 437 | * Month dropdown template. 438 | */ 439 | 440 | function monthDropdown(months) { 441 | var options = map(months, monthOption).join(''); 442 | return ''; 443 | } 444 | 445 | /** 446 | * Year dropdown option template. 447 | */ 448 | 449 | function yearOption(year) { 450 | return ''; 451 | } 452 | 453 | /** 454 | * Month dropdown option template. 455 | */ 456 | 457 | function monthOption(month, i) { 458 | return ''; 459 | } 460 | --------------------------------------------------------------------------------