Create a calendar from which dates can be selected.
Option name | Type | Description |
---|---|---|
module | helper/calendar.js |
new Calendar(el);
Calendar constructor
Option name | Type | Description |
---|---|---|
el | Element | Optional |
params | Object | Optional |
function Calendar(el, params) {
params = params || {};
// If the first element is an array or array like (NodeList),
// we will be working with a range.
if (el && el.hasOwnProperty('length')) {
params.els = el;
}
// If the first argument is a plain object, create a default element
// since the user MUST provide additional params but the element
// is optional. Doing it this way to keep the arity the same
// as other components.
else if (!(el instanceof HTMLElement)) {
params = el || {};
params.els = [this._createDefaultElement()];
}
// A single element is passed.
else {
params.els = [el];
}
params.visibleCounts = params.visibleCounts || [params.visibleCount] || null;
params.mins = params.mins || [params.min] || null;
params.maxes = params.maxes || [params.max] || null;
params.values = params.values || [params.value] || null;
this._setParams(this.defaults, true);
this._setParams(params);
this._parseParams();
this._bindEventListenerCallbacks();
// Create a calendar element if we weren't passed one.
(this.calendarEl ? noop : this._createCalendar).call(this);
}
Calendar.prototype = {
Include common functionality.
_setParams: Base.setParams,
_appendChildren: Base.appendChildren,
remove: Base.remove,
Whitelisted parameters which can be set on construction.
Option name | Type | Description |
---|
_whitelistedParams: ['els', 'visibleCounts', 'advance', 'mins', 'maxes', 'values', 'daysDisabled', 'daysInfo', 'calendarEl', 'viewRange', 'weekStartsOn', 'monthNames', 'monthNamesShort', 'dayInitials', 'dayNames', 'onChange'],
Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.
Option name | Type | Description |
---|
defaults: {
els: null,
calendarEl: null,
visibleCounts: null,
activeIndex: null,
mins: null,
maxes: null,
values: null,
daysDisabled: {2016: {3: [9]}},
daysInfo: {2016: {3: {10: 'A note'}}},
viewRange: 'month',
weekStartsOn: 'sunday',
monthNamesShort: null,
monthNames: null,
dayInitials: null,
dayNames: null,
advance: true,
_daysCache: null,
_weeksCache: null,
_monthsCache: null,
_yearsCache: null,
_onChangeBound: null
},
Open the calendar widget.
Option name | Type | Description |
---|---|---|
index | Number | Optional The index of the element to tie actions to |
open: function(index) {
index = index || 0;
// Already open
if (this.activeIndex === index) {
return;
}
// Set the new index
this.activeIndex = index;
// If we don't have a popover yet, create it.
if (!this.popover) {
this._createPopover();
}
// Create a calendar or calendars inside the popover
this.render();
// Finally, show the popover.
this.popover.open();
},
Close the calendar widget.
close: function() {
if (this.activeIndex === null || !this.popover) {
return;
}
this.popover.close();
this.activeIndex = null;
},
Get the value.
Option name | Type | Description |
---|---|---|
asInt | Boolean | Get the value as a parsed integer. |
return | Mixed |
getValue: function(asInt) {
return asInt ? parseInt(this.select.getValue(), 10) : this.select.getValue();
},
Set the date for a given element.
Option name | Type | Description |
---|---|---|
value | String, Object | |
index | Number, Element |
setValue: function(value, index) {
if (!value) {
return;
}
var el;
// If we are passed an element instead of an index, use that.
if (index instanceof HTMLElement) {
el = index;
index = this.els.indexOf(el);
}
// Otherwise, find the element in our list.
else {
el = this.els[index || 0];
}
// Parse the value into an object if we need to.
value = typeof value === 'object' ? parsedDomFormat.getString(value) : value;
// Set the value on the input element
el.value = value;
// Update the value on the calendar
this.updateValues(index);
},
Take the date values from the inputs and set them as dates on the calendar.
Option name | Type | Description |
---|---|---|
index | Number | Optional A single index to update |
updateValues: function(index) {
this.values = this.values || [];
var els = index === undefined ? this.els : [this.els[index]];
var i = index || 0;
var len = (index || 0) + els.length;
for (; i < len; i++) {
this.values[i] = this.els[i].value ? parsedDomFormat.getValues(this.els[i].value) : null;
}
},
Render the calendar or calendars into the popover.
render: function() {
// If we don't have a popover yet, create it.
if (!this.popover) {
this._createPopover();
}
// Ensure the cached values are in line with the input values.
this.updateValues();
// Get the right number of dates to show based on the viewRange and visibleCounts
var dates = this._getVisibleDates();
var content;
// Create the visible days, weeks, months or years
if (this.viewRange === 'year')
content = this._renderYears(dates);
else if (this.viewRange === 'week')
content = this._renderWeeks(dates);
else if (this.viewRange === 'day')
content = this._renderDays(dates);
else
content = this._renderMonths(dates);
this._appendChildren(this.calendarEl, content, true);
},
Render the appropriate number of years.
Option name | Type | Description |
---|---|---|
dates | Array | |
return | Array |
_renderYears: function(
dates
{
},
Render the appropriate number of weeks.
Option name | Type | Description |
---|---|---|
dates | Array | |
return | Array |
_renderWeeks: function(
dates
{
},
Render the appropriate number of days.
Option name | Type | Description |
---|---|---|
dates | Array | |
return | Array |
_renderDays: function(
dates
{
},
Render the appropriate number of months.
Option name | Type | Description |
---|---|---|
dates | Array | |
return | Array |
_renderMonths: function(dates) {
var months = [];
var i = 0;
var len = dates.length;
for (; i < len; i++) {
months.push(this._renderMonth(dates[i]));
}
return months;
},
Render a month.
Option name | Type | Description |
---|---|---|
date | Object | |
return | Element |
_renderMonth: function(date) {
// A unique key for this month used for caching
var key = date.year + '-' + date.month;
// Ensure we have a cache
this._monthsCache = this._monthsCache || [];
// Return a cached instance
if (this._monthsCache[key]) {
return this._monthsCache[key];
}
// Create the element
var el = document.createElement('div');
el.className = 'spark-calendar__month';
// Add the title
el.innerHTML += '<div class="spark-calendar__month-title">' + dates.getMonthName(date.month) + ' ' + date.year + '</div>';
// Add the days of the week headings
el.innerHTML += '<div class="spark-calendar__days-of-week">';
el.innerHTML += this._renderMonthDaysOfWeek();
el.innerHTML += '</div>';
// Add the days
el.innerHTML += '<div class="spark-calendar__days">';
el.innerHTML += this._renderMonthDays(date);
el.innerHTML += '</div>';
// Store in the cache and return
return (this._monthsCache[key] = el);
},
Render the days of week row for a month calendar.
Option name | Type | Description |
---|---|---|
return | String |
_renderMonthDaysOfWeek: function() {
var daysOfWeek = dates.getDayNames();
var i = 0;
var len = daysOfWeek.length;
var str = '<div class="spark-calendar__days-of-week">';
for (; i < len; i++) {
str += '<span class="spark-calendar__day-of-week">' + daysOfWeek[i][0] + '</span>';
}
return str += '</div>';
},
Render the days of week row for a month calendar.
Option name | Type | Description |
---|---|---|
date | Object | |
return | String |
_renderMonthDays: function(date) {
var current = dates.getCurrentDate();
var dayOfWeek = dates.getDayOfWeek({year: date.year, month: date.month, day: 1});
var startOfWeek = dayOfWeek > 0 ? dates.getWeekStart({year: date.year, month: date.month, day: 1}) : null;
var monthEnd = dates.getMonthEnd(date);
var weeks = 6;
var i = 0;
var j = 0;
var str = '<div class="spark-calendar__days">';
var day = 0;
var isCurrentMonth = current.year === date.year && current.month === date.month;
var inactive = startOfWeek;
for (; i < weeks; i++) {
for (; j < 7; j++) {
day = startOfWeek ? startOfWeek.day + j : day + 1;
str += this._renderMonthDay(day, isCurrentMonth, current, inactive);
if (startOfWeek && j + 1 >= dayOfWeek) {
startOfWeek = null;
inactive = null;
day = 0;
}
else if (day >= monthEnd.day) {
inactive = true;
day = 0;
}
}
j = 0;
}
return str += '</div>';
},
Render a day of the month.
_renderMonthDay: function(day, isCurrentMonth, current, inactive) {
var str = '<a class="spark-calendar__day';
str += (isCurrentMonth && current.day === day ? ' spark-calendar__day--today' : '');
str += (inactive ? ' spark-calendar__day--inactive' : '');
str += '"';
str += (inactive || this._isDayDisabled(day, current.month, current.year) ? ' disabled' : '');
str += ' href="#">';
str += day ;
str += this._getDayInfo(day, current.month, current.year);
str += '</a>';
return str;
},
Is a given day disabled?
Option name | Type | Description |
---|---|---|
day | Number | |
month | Number | |
year | Number | |
return | Boolean |
_isDayDisabled: function(day, month, year) {
return this.daysDisabled && this.daysDisabled[year] && this.daysDisabled[year][month] && this.daysDisabled[year][month].indexOf(day) !== -1;
},
Get any "info" for a given day.
Option name | Type | Description |
---|---|---|
day | Number | |
month | Number | |
year | Number | |
return | String |
_getDayInfo: function(day, month, year) {
return this.daysInfo && this.daysInfo[year] && this.daysInfo[year][month] && this.daysInfo[year][month][day] ? this.daysInfo[year][month][day] : '';
},
Create the default input element.
Option name | Type | Description |
---|---|---|
type | String | |
return | Element |
_createDefaultElement: function() {
var el = document.createElement('span');
el.className = 'spark-input spark-date';
el.innerHTML = '<input class="spark-input__field" type="date"><span class="spark-label"></span>';
return el;
},
Create the calendar.
_createCalendar: function() {
var el = document.createElement('div');
el.className = 'spark-calendar';
el.innerHTML = '<div class="spark-calendar__container"><nav class="spark-calendar__nav"><button class="spark-calendar__previous spark-icon-chevron-left"></button><button class="spark-calendar__next spark-icon-chevron-right"></button></nav></div>';
this.calendarEl = el;
},
Create the popover.
_createPopover: function() {
this.popover = new Popover(this.els[0], {
direction: 'bottom',
contentEl: this.calendarEl
});
},
Parse parameters from the elements.
_parseParams: function() {
this._parseElsParams();
},
Parse the min, max, value and visible counts from the elements if we can.
Option name | Type | Description |
---|---|---|
return | Number, Boolean |
_parseElsParams: function() {
if (!this.els) {
return;
}
var i = 0;
var len = this.els.length;
var visible = [];
var mins = [];
var maxes = [];
var values = [];
for (; i < len; i++) {
if (this.visibleCounts[i])
visible[i] = this.visibleCounts[i];
else
visible[i] = parseInt(this.els[i].getAttribute('data-visible-count'), 10) || 1;
if (this.mins && this.mins[i])
mins[i] = typeof this.mins[i] === 'object' ? this.mins[i] : parsedDomFormat.getValues(this.mins[i]);
else if (this.els[i].getAttribute('min'))
mins[i] = parsedDomFormat.getValues(this.els[i].getAttribute('min'));
if (this.maxes && this.maxes[i])
maxes[i] = typeof this.maxes[i] === 'object' ? this.maxes[i] : parsedDomFormat.getValues(this.maxes[i]);
else if (this.els[i].getAttribute('max'))
maxes[i] = parsedDomFormat.getValues(this.els[i].getAttribute('max'));
if (this.values && this.values[i])
values[i] = typeof this.values[i] === 'object' ? this.values[i] : parsedDomFormat.getValues(this.values[i]);
else if (this.els[i].value)
values[i] = parsedDomFormat.getValues(this.els[i].value);
}
this.visibleCounts = visible;
this.mins = mins;
this.maxes = maxes;
this.values = values;
},
Get the dates we should be showing. Start with the first value
or today's date.
Option name | Type | Description |
---|---|---|
return | Array | An array of date objects. |
_getVisibleDates: function() {
var dateArr = [];
var i = 0;
var len = this.values.length;
var method;
// Get the first month
for (; i < len && !dateArr.length; i++) {
if (this.values[i]) {
dateArr.push(this.values[i]);
}
}
// If we didn't get a month, add today's date.
if (!dateArr.length) {
dateArr.push(dates.getCurrentDate());
}
// If we are supposed to show more months
if (dateArr.length < this.visibleCounts[this.activeIndex || 0]) {
// Add the correct number of visible dates
for (i = dateArr.length; i < this.visibleCounts[this.activeIndex || 0]; i++) {
if (this.viewRange === 'year')
method = dates.getNextYear;
else if (this.viewRange === 'week')
method = dates.getNextWeek;
else if (this.viewRange === 'day')
method = dates.getNextDay;
else
method = dates.getNextMonth;
dateArr.push(method.call(dates, dateArr[i-1]));
}
}
return dateArr;
},
Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.
_bindEventListenerCallbacks: function() {
this._onChangeBound = this._onChange.bind(this);
},
When the calendar value changes, run a callback.
_onChange: function() {
(this.onChange || noop)();
}
};
Base.exportjQuery(Calendar, 'Calendar');
return Calendar;
}));