├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------