////////////////////////////////////////////////////////////////////////////////
//
// DatePicker v0.3
//
// Author:
// Leblanc Christophe
//
// Description:
// Pure Javascript date picker/ calendar for JS/HTML applications or websites.
//
// Todo: Improve readability
//
// Last 5 Updates:
// -01/05/2018 : improvements on readability
// -17/02/2018 : add merging properties possibilities
//
// License:
// -This code is distributed for free without any warrantee.
// -You can use it, modify it, redistribute it, at your own risks, without other restrictions
// than naming the original author.
//
// -Feedbacks and opinions are allways appreciated
//
////////////////////////////////////////////////////////////////////////////////
function CJPLDatePicker(id, user_properties){
const config = {
/* Some languages variables */
MONTHS : ['Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre'], //Note: Sunday is 0, Monday is 1, and so on.
WEEKDAYS : ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'], //Note: Sunday is 0, Monday is 1, and so on.
WEEKDAYS_MIN : ['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di']
}
if(user_properties === undefined){
user_properties = null;
}
/* Declare a common reference of this object */
var picker = this;
/* Declare some internal "objects" */
var Properties = {};
var Events = {};
var Calendar = {};
var HTML = {};
/* Variables */
var properties;
var parent;
var view;
var top;
var viewMonth;
var viewTable;
/* Function to be overloaded by users */
this.onSelect = function(){}
/* Output selection for users */
this.selection = {
dateObject: null,
year: 0,
month: 0,
date: 0,
timestamp : 0
};
/* Defaults properties */
Properties.defaults = {
viewDate: null,
selectedDate: null,
classNames: {
main: 'date_picker',
table: 'table'
}
}
/**
* Merge properties objects
* @param {object} defaultProps - The default properties object
* @param {object} newProps - The default properties object
* @returns {object} The default property object with the new properties
*/
Properties.merge = function(defaultProps, newProps) {
for (var key in defaultProps) {
if (newProps.hasOwnProperty(key)){
if(key == 'classNames'){//In case of sub properties
Properties.merge(defaultProps[key], newProps[key]);
}
else{
defaultProps[key] = newProps[key];//Common case
}
}
}
return defaultProps;
}
/**
* The top part of the DatePicker
* @param {object} par - The DOMElement object which is parent of this part
* @returns {HTML.Top}
*/
HTML.Top = function(par){
var elt_c = document.createElement('div');
elt_c.className = 'top';
var elt_l = document.createElement('div');
elt_l.className = 'cell side left unselectable';
elt_l.innerHTML = '<';
elt_c.appendChild(elt_l);
var elt_m = document.createElement('div');
elt_m.className = 'cell mid';
elt_m.innerHTML = 'M';
elt_c.appendChild(elt_m);
var elt_r = document.createElement('div');
elt_r.className = 'cell side right unselectable';
elt_r.innerHTML = '>';
elt_c.appendChild(elt_r);
par.appendChild(elt_c);
this.container_element = elt_c;
this.leftElement = elt_l;
this.centerElement = elt_m;
this.rightElement = elt_r;
}
/**
* Create the top part of the calendar
* @param {object} par - The DOMElement object which is parent of this part
* @returns {HTML.Top}
*/
HTML.Top.create = function(par){
return new HTML.Top(par);
}
/**
* Create the table of days for a month
* @param {object} par - The DOMElement object which is parent of this part
* @param {object} calendarMonth - The Calendar.Month object to display
* @returns {HTML.Table}
*/
HTML.Table = function(par, calendarMonth){
var th_ = this;
this.rows;
this.cells;
/**
* Check if a given date in the table is the selected date and add a 'selected' class name to the cell if it is
* @param {object} element - The DOMElement of the cell of the date
* @param {object} date - The date
*/
function checkAndWriteCellSelection(element, date){
if(date.getFullYear() == picker.selection.year){
if(date.getMonth() == picker.selection.month){
if(date.getDate() == picker.selection.date){
element.className += ' selected';
}
}
}
}
/**
* Create a cell for a day
* @param {object} parentElement - The DOMElement of the parent of this cell
* @param {object} date - The date of the day
* @param {boolean} styleOut - If the day is "outside" the current month
* @param {number} rowIndex - The index of the row of this cell
* @param {number} cellIndex - The index of the cell
* @returns {object} - The DOMElement of this cell
*/
function createDayCell(parentElement, date, styleOut, rowIndex, cellIndex){
var cellElement = document.createElement('div');
cellElement.className = (styleOut == true) ? 'day out' : 'day';
cellElement.innerHTML = '' + date.getDate() + '';
checkAndWriteCellSelection(cellElement, date);
cellElement.onclick = function(){
selectCell(rowIndex, cellIndex);
}
parentElement.appendChild(cellElement);
return cellElement;
}
/**
* Create a row of the table
* @param {object} parentElement - The DOMElement of the parent of this cell
* @returns {object} - The DOMElement of this row
*/
function createMonthRow(parentElement){
var rowElement = document.createElement('div');
rowElement.className = 'row';
parentElement.appendChild(rowElement);
return rowElement;
}
/**
* Create the head row of the table
* @param {object} parentElement - The DOMElement of the parent of this cell
* @returns {object} - The DOMElement of this row
*/
function createTableHead(parentElement){
var rowElement = document.createElement('div');
rowElement.className = 'row head';
for(var i = 0; i < 7; i++){
var cellElement = document.createElement('div');
cellElement.className = 'label';
cellElement.innerHTML = '' + config.WEEKDAYS_MIN[i] + '';
rowElement.appendChild(cellElement);
}
parentElement.appendChild(rowElement);
return rowElement;
}
/**
* Set a new month in this table
* @param {object} calendarMonth - The new Calendar.Month object of this table
*/
this.setMonth = function(calendarMonth){
var it = new Calendar.Month.Iterator(calendarMonth);
var styleout = [true, false, true];
var cur_date;
/* Iterate the dates with a little trick to determine at same time if dates are inside the current month or not */
for(var i = 0; i < 6; i++){
for(var j = 0; j < th_.cells[i].length; j++){
cur_date = it.get();
th_.cells[i][j].innerHTML = '' + cur_date.getDate() + '';
th_.cells[i][j].className = (styleout[it.space] == true) ? 'day out' : 'day';
checkAndWriteCellSelection(th_.cells[i][j], cur_date);
it.next();
}
}
}
/**
* Select a cell of the table
* @param {object} calendarMonth - The new Calendar.Month object of this table
* @param {number} rowIndex - The index of the row of the selected cell
* @param {number} cellIndex - The index of the selected cell
*/
this.select = function(calendarMonth, rowIndex, cellIndex){
var x = 0;
var y = 0;
var it = new Calendar.Month.Iterator(calendarMonth);
var styleout = [true, false, true];
var length = 0;
/* Iterate the dates with a little trick to determine at same time if dates are inside the current month or not */
for(var i = 0; i < 42; i++){
length++;
th_.cells[x][y].className = (styleout[it.space] == true) ? 'day out' : 'day';
if(x == rowIndex && y == cellIndex){
th_.cells[x][y].className += ' selected';
}
it.next();
if(y == 6){y = 0;x++;}else{y++;}
}
}
/**
* Create this HTML.Table
*/
function create(){
/* Create the head row */
var head_row = createTableHead(par);
/* Create 6 rows */
th_.rows = [];
th_.cells = [];
for(var i = 0; i < 6; i++){
th_.rows[i] = createMonthRow(par);
th_.cells[i] = [];
}
/* Fill the rows */
var x = 0;
var y = 0;
var it = new Calendar.Month.Iterator(calendarMonth);
var styleout = [true, false, true];
var length = 0;
/* Iterate the dates with a little trick to determine at same time if dates are inside the current month or not */
for(var i = 0; i < 42; i++){
th_.cells[x][y] = createDayCell(th_.rows[x], it.get(), styleout[it.space], x, y);
length++;
it.next();
if(y == 6){y = 0;x++;}else{y++;}
}
delete it;
}
create();
}
/**
* Class representing a month in the calendar
* @param {Array.Date} daysBefore - An array of dates before the current month
* @param {Array.Date} daysIn - An array of dates in the current month
* @param {Array.Date} daysAfter - An array of dates after the current month
* @param {number} length - The total number of days
*/
Calendar.Month = function(daysBefore, daysIn, daysAfter, length){
this.daysIn = daysIn;
this.daysBefore = daysBefore;
this.daysAfter = daysAfter;
this.length = length;
this.getAt = function(index){
if(index < viewMonth.daysBefore.length){
return viewMonth.daysBefore[index];
}
else{
if(index < viewMonth.daysBefore.length + viewMonth.daysIn.length){
index = index - viewMonth.daysBefore.length;
return viewMonth.daysIn[index];
}
else{
index = index - (viewMonth.daysBefore.length + viewMonth.daysIn.length);
return viewMonth.daysAfter[index];
}
}
}
}
/**
* Create a Calendar.Month object for a month
* @param {number} year - The year of the month to create
* @param {number} month - The month
* @return {Calendar.Month} The Calendar.Month created
*/
Calendar.createMonth = function(year, month){
var cur_array = [];
var pre_array = [];
var post_array = [];
var month_date = new Date(year, month);
/* Compute the current month */
var length = 0;
var runnin = true;
var cur_day = 1;
var first = true;
var last_date;
var i = 0;
var first_monday_idx = null;
while(runnin){
var new_date = new Date(year, month, cur_day);
if(!last_date){last_date = month_date;}
if(new_date.getMonth() == last_date.getMonth() || first){
if(first){first = false;}
if(new_date.getDay() == 1 && first_monday_idx == null){
first_monday_idx = i;
}
cur_array[i] = new_date;
cur_day++;
i++;
last_date = new_date;
}
else{
runnin = false;
}
}
length += cur_array.length;
/* Compute the pre month period days */
var first_day = cur_array[0];
var first_monday = cur_array[first_monday_idx];
var count = (first_monday.getDate() == 1) ? 7 : 7 - (first_monday.getDate() - 1);//Store a complete week if first date == 1
for(var i = 0; i < count; i++){
var new_date = createDateAdd(first_day, - (count - ( i)));
pre_array[i] = new_date;
}
length += pre_array.length;
/* Compute the post month period days */
var last_day = cur_array[cur_array.length-1];
count = 42 - length;
for(var i = 0; i < count; i++){
var new_date = createDateAdd(last_day, i + 1);
post_array[i] = new_date;
}
length += post_array.length;
//console.log('CREATE pre ' + pre_array.length + ' cur ' + cur_array.length + ' post ' + post_array.length)
return new Calendar.Month(pre_array, cur_array, post_array, length);
}
/**
* Create a Calendar.Month iterator
* @param {Calendar.Month} month - The month in which this operator must operate
* @return {Calendar.Month.Iterator}
*/
Calendar.Month.Iterator = function(month){
var daysIn = month.daysBefore;
this.index = 0;
this.space = 0;
this.get = function(){
return daysIn[this.index];
}
this.next = function(){
if(this.index == daysIn.length-1){
this.index = 0;
if(this.space == 0){
daysIn = month.daysIn;
this.space++;
}
else if(this.space == 1){
daysIn = month.daysAfter;
this.space++;
}
}
else{
this.index++;
}
}
}
/**
* Create a Calendar.Month object from a Date object
* @param {Date} date - The Date object used to create the month
* @return {Calendar.Month} The Calendar.Month created
*/
Calendar.createMonthFromDate = function(date){
return Calendar.createMonth(date.getFullYear(), date.getMonth());
}
/**
* Create a new date after a given number of days
* @param {Date} date - The Date object used to create the month
* @param {number} nDays - The number of days after the Date object "date"
* @return {Date} The calculated date
*/
function createDateAdd(date, nDays){
return new Date(date.getTime()+(nDays*24*60*60*1000));
}
/**
* Go to the previous month in the DatePicker
*/
function decreaseMonthView(){
if(view.month == 0){
view.month = 11;
view.year--;
}
else{
view.month--;
}
}
/**
* Go to the next month in the DatePicker
*/
function increaseMonthView(){
if(view.month == 11){
view.month = 0;
view.year++;
}
else{
view.month++;
}
}
/**
* Update the top part of the DatePicker
*/
function updateTop(){
top.centerElement.innerHTML = config.MONTHS[view.month] + ' ' + view.year;
}
/**
* Load the view of the current month
*/
function loadMonthView(){
view.dateObject = new Date(view.year, view.month, 1);
updateTop();
viewMonth = Calendar.createMonthFromDate(view.dateObject);
viewTable.setMonth(viewMonth);
}
/**
* Select a cell of the table
* @param {number} rowIndex - The index of the row of the selected cell
* @param {number} cellIndex - The index of the selected cell
*/
function selectCell(rowIndex, cellIndex){
var index = (rowIndex * 7) + cellIndex;
picker.selection.dateObject = viewMonth.getAt(index);
picker.selection.year = picker.selection.dateObject.getFullYear();
picker.selection.month = picker.selection.dateObject.getMonth();
picker.selection.date = picker.selection.dateObject.getDate();
picker.selection.timestamp = picker.selection.dateObject.getTime();
viewTable.select(viewMonth, rowIndex, cellIndex);
picker.onSelect();
}
/**
* " Constructor "
*/
function create(){
/* Get properties from user or defaults */
properties = (user_properties != null) ? Properties.merge(Properties.defaults, user_properties) : Properties.defaults;
parent = document.getElementById(id);
view = {
dateObject: null,
year: 0,
month: 0,
date: 0
};
var calendarElement = document.createElement('div');
calendarElement.className = properties.classNames.main;
top = HTML.Top.create(calendarElement);
/* Create events */
top.leftElement.onclick = function(){
decreaseMonthView();
loadMonthView();
}
top.rightElement.onclick = function(){
increaseMonthView();
loadMonthView();
}
var daysTableElement = document.createElement('div');
daysTableElement.className = properties.classNames.table;
calendarElement.appendChild(daysTableElement);
parent.appendChild(calendarElement);
/* Get start dates and create the calendar around it */
var startDate;
var viewStartDate;
if(properties.viewDate != null && typeof properties.viewDate == 'Date'){
viewStartDate = properties.viewDate;
}
else{
startDate = new Date();
viewStartDate = startDate;
}
var selectionStartDate;
if(properties.selectedDate != null && typeof properties.selectedDate == 'Date'){
selectionStartDate = properties.selectedDate;
}
else{
if(startDate == null){startDate = new Date();}
selectionStartDate = startDate;
}
picker.selection.year = selectionStartDate.getFullYear();
picker.selection.month = selectionStartDate.getMonth();
picker.selection.date = selectionStartDate.getDate();
picker.selection.timestamp = selectionStartDate.getTime();
picker.selection.dateObject = new Date(picker.selection.year, picker.selection.month, picker.selection.date);
view.year = viewStartDate.getFullYear();
view.month = viewStartDate.getMonth();
view.date = viewStartDate.getDate();
viewMonth = Calendar.createMonthFromDate(viewStartDate);
viewTable = new HTML.Table(daysTableElement, viewMonth);
view.dateObject = new Date(view.year, view.month, 1);
updateTop();
}
create();
}
/**
* Create an instance of CJPLDatePicker
* @param {string} id - The id of the element to display the DatePicker
* @param {object} user_properties - An object containing some properties for the DatePicker
*/
CJPLDatePicker.create = function(id, user_properties){
return new CJPLDatePicker(id, user_properties);
}