Difference between pages "JavaScript - Binding" and "JavaScript - Calendar Date Picker"

From NoskeWiki
(Difference between pages)
Jump to navigation Jump to search
(Created page with "==About== {{DaughterPage|mother=JavaScript}} JavaScript binding always messes me up... I don't do JavaScript quite often enough to know the flow well... so typically I'll...")
 
(Created page with "254px|thumb|Date picker demo (datepickerdemo.html) in action. ==About== {{DaughterPage|mother=JavaScript}} A common task in onl...")
 
Line 1: Line 1:
 +
[[Image:Javascript_date_picker_tool.png|254px|thumb|Date picker demo (datepickerdemo.html) in action.]]
 +
 
==About==
 
==About==
 
{{DaughterPage|mother=[[JavaScript]]}}
 
{{DaughterPage|mother=[[JavaScript]]}}
  
JavaScript binding always messes me up... I don't do JavaScript quite often enough to know the flow well... so typically I'll have a loop, each one calling a function with a different index (or something different), yet they all come out the same... below is a nice example from [https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example this thread] on what's happening:
+
A common task in online forms is to enter a date <i>(often for DOB, but often for reservations)</i>, and to help users select the correct date is to have a calendar popup. I found a really [https://github.com/joshsalverda/datepickr fantastic little datepickr class] by <b>Josh Salverda</b> (who I've credited below) and uses fairly minimal code: 1 x .js file and 1 x .css file, with ~ 600 total lines <i>(not bad considering all the necessary logic for calendar days etc)</i>. I've taken this code, modified it a little, to conform to [https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml Google's JavaScript Style Guide] and provided it below as a nice base you can copy and paste, and then customize by playing with the styles etc.
 +
 
 +
 
 +
There are several large JavaScript libraries which include date pickers, for example [https://jqueryui.com/datepicker/ JQuery UI's Datepicker], and there are also many other independent date picker scripts such as those listed [http://www.hongkiat.com/blog/useful-calendar-date-picker-scripts-for-web-developers/ here].... but I really like Josh's visual style and so this is the one I chose and I'm sure I'll reuse again!  :)
 +
 
 +
 
 +
{{Prettyblockquote}}
 +
NOTE: In HTML5 they have added a new "date" type for the [http://www.w3schools.com/tags/att_input_type.asp input tag] which adds a date picker for you.... however won't work for older browsers. The one line of code is:
 +
 
 +
<code><input type="date" name="my_date" id="my_date"></code>
 +
</blockquote>
 +
 
 +
==Date Picker Code==
 +
 
 +
INSTRUCTIONS: Create the following three files in the same dir and you should be good to go!
 +
 
 +
See [http://joshsalverda.github.io/datepickr/ more involved demo working here].
 +
 
  
==Binding==
 
  
 +
<b>datepickerdemo.html:</b>
 +
<syntaxhighlight lang="html5">
 +
<!DOCTYPE html>
 +
<html>
 +
  <head>
 +
    <title>Date Picker Demo</title>
 +
    <link rel="stylesheet" href="datepicker.css">
 +
  </head>
 +
  <body>
 +
    <h3>Date Picker:</h3>
 +
    <input id="datepick" size="10" class="datepicker" placeholder="mm/dd/yyyy">
 +
    <script src="datepicker.js"></script>
 +
    <script>
 +
      // Custom date format
 +
      DatePicker('.datepicker', { dateFormat: 'm/d/Y'});
 +
    </script>
 +
  </body>
 +
</html>
 +
</syntaxhighlight>
  
A loop without binding:
+
----
  
 +
<b>datepicker.js:</b>
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 +
/**
 +
* @fileoverview Date picker popup calendar widget.
 +
*
 +
* CREDIT:
 +
*  ~ code based on: datepickr 3.0
 +
*  ~ authored by:  Josh Salverda <josh.salverda@gmail.com>
 +
*  ~ available at:  https://github.com/joshsalverda/datepickr
 +
*  ~ licence:      open (http://www.wtfpl.net/)
 +
*
 +
* Code was then adjusted by Andrew Noske (andrew.noske@gmail / anoske@)
 +
* to conform to Google's JavaScript Style Guide.
 +
*/
 +
 +
/**
 +
* Apply datepicker popup to all do elements matching the selector.
 +
* @param {Object} selector Selector for DOM elements to apply to
 +
*    (eg: '.datepicker' - for all elements matching this class).
 +
* @param {Object} config Configuration values for this
 +
*    specific instance (eg: '{ dateFormat: 'm/d/Y'}').
 +
* @constructor
 +
*/
 +
var DatePicker = function(selector, config) {
 +
  'use strict';
 +
  var elements,
 +
    createInstance,
 +
    instances = [],
 +
    i;
 +
 +
  DatePicker.prototype = DatePicker.init.prototype;
 +
 +
  createInstance = function(element) {
 +
    if (element._DatePicker) {
 +
      element._DatePicker.destroy();
 +
    }
 +
    element._DatePicker = new DatePicker.init(element, config);
 +
    return element._DatePicker;
 +
  };
 +
 +
  if (selector.nodeName) {
 +
    return createInstance(selector);
 +
  }
 +
 +
  elements = DatePicker.prototype.querySelectorAll(selector);
 +
 +
  if (elements.length === 1) {
 +
    return createInstance(elements[0]);
 +
  }
 +
 +
  for (i = 0; i < elements.length; i++) {
 +
    instances.push(createInstance(elements[i]));
 +
  }
 +
  return instances;
 +
};
 +
 +
/**
 +
* Apply datepicker popup to a particular element.
 +
* @param {Object} element DOM element on which this operates.
 +
* @param {Object} instanceConfig Configuration values for this
 +
*    specific instance (eg: '{ dateFormat: 'm/d/Y'}').
 +
* @constructor
 +
*/
 +
DatePicker.init = function(element, instanceConfig) {
 +
  'use strict';
 +
  var self = this,
 +
    defaultConfig = {
 +
      dateFormat: 'F j, Y',
 +
      altFormat: null,
 +
      altInput: null,
 +
      minDate: null,
 +
      maxDate: null,
 +
      shorthandCurrentMonth: false
 +
    },
 +
    calendarContainer = document.createElement('div'),
 +
    navigationCurrentMonth = document.createElement('span'),
 +
    calendar = document.createElement('table'),
 +
    calendarBody = document.createElement('tbody'),
 +
    wrapperElement,
 +
    currentDate = new Date(),
 +
    wrap,
 +
    date,
 +
    formatDate,
 +
    monthToStr,
 +
    isSpecificDay,
 +
    buildWeekdays,
 +
    buildDays,
 +
    updateNavigationCurrentMonth,
 +
    buildMonthNavigation,
 +
    handleYearChange,
 +
    documentClick,
 +
    calendarClick,
 +
    buildCalendar,
 +
    getOpenEvent,
 +
    bind,
 +
    open,
 +
    close,
 +
    destroy,
 +
    init;
 +
 +
  calendarContainer.className = 'datepicker-calendar';
 +
  navigationCurrentMonth.className = 'datepicker-current-month';
 +
  instanceConfig = instanceConfig || {};
 +
 +
  wrap = function() {
 +
    wrapperElement = document.createElement('div');
 +
    wrapperElement.className = 'datepicker-wrapper';
 +
    self.element.parentNode.insertBefore(wrapperElement, self.element);
 +
    wrapperElement.appendChild(self.element);
 +
  };
 +
 +
  date = {
 +
    current: {
 +
      year: function() {
 +
        return currentDate.getFullYear();
 +
      },
 +
      month: {
 +
        integer: function() {
 +
          return currentDate.getMonth();
 +
        },
 +
        string: function(shorthand) {
 +
          var month = currentDate.getMonth();
 +
          return monthToStr(month, shorthand);
 +
        }
 +
      },
 +
      day: function() {
 +
        return currentDate.getDate();
 +
      }
 +
    },
 +
    month: {
 +
      string: function() {
 +
        return monthToStr(self.currentMonthView,
 +
                          self.config.shorthandCurrentMonth);
 +
      },
 +
      numDays: function() {
 +
        // Checks to see if february is a leap year otherwise
 +
        // return the respective # of days.
 +
        return self.currentMonthView === 1 &&
 +
            (((self.currentYearView % 4 === 0) &&
 +
            (self.currentYearView % 100 !== 0)) ||
 +
            (self.currentYearView % 400 === 0)) ?
 +
            29 : self.l10n.daysInMonth[self.currentMonthView];
 +
      }
 +
    }
 +
  };
 +
 +
  formatDate = function(dateFormat, milliseconds) {
 +
    var formattedDate = '',
 +
      dateObj = new Date(milliseconds),
 +
      formats = {
 +
        d: function() {
 +
          var day = formats.j();
 +
          return (day < 10) ? '0' + day : day;
 +
        },
 +
        D: function() {
 +
          return self.l10n.weekdays.shorthand[formats.w()];
 +
        },
 +
        j: function() {
 +
          return dateObj.getDate();
 +
        },
 +
        l: function() {
 +
          return self.l10n.weekdays.longhand[formats.w()];
 +
        },
 +
        w: function() {
 +
          return dateObj.getDay();
 +
        },
 +
        F: function() {
 +
          return monthToStr(formats.n() - 1, false);
 +
        },
 +
        m: function() {
 +
          var month = formats.n();
 +
          return (month < 10) ? '0' + month : month;
 +
        },
 +
        M: function() {
 +
          return monthToStr(formats.n() - 1, true);
 +
        },
 +
        n: function() {
 +
          return dateObj.getMonth() + 1;
 +
        },
 +
        U: function() {
 +
          return dateObj.getTime() / 1000;
 +
        },
 +
        y: function() {
 +
          return String(formats.Y()).substring(2);
 +
        },
 +
        Y: function() {
 +
          return dateObj.getFullYear();
 +
        }
 +
      },
 +
      formatPieces = dateFormat.split('');
 +
 +
    self.forEach(formatPieces, function(formatPiece, index) {
 +
      if (formats[formatPiece] && formatPieces[index - 1] !== '\\') {
 +
        formattedDate += formats[formatPiece]();
 +
      } else {
 +
        if (formatPiece !== '\\') {
 +
          formattedDate += formatPiece;
 +
        }
 +
      }
 +
    });
 +
 +
    return formattedDate;
 +
  };
 +
 +
  monthToStr = function(date, shorthand) {
 +
    if (shorthand === true) {
 +
      return self.l10n.months.shorthand[date];
 +
    }
 +
 +
    return self.l10n.months.longhand[date];
 +
  };
  
var funcs = [];
+
  isSpecificDay = function(day, month, year, comparison) {
for (var i = 0; i < 3; i++) {     // Let's create 3 functions
+
    return day === comparison && self.currentMonthView === month &&
  funcs[i] = function() {          // ... and store them in funcs
+
        self.currentYearView === year;
    console.log("My value: " + i); // ... each should log its value.
 
 
   };
 
   };
 +
 +
  buildWeekdays = function() {
 +
    var weekdayContainer = document.createElement('thead'),
 +
      firstDayOfWeek = self.l10n.firstDayOfWeek,
 +
      weekdays = self.l10n.weekdays.shorthand;
 +
 +
    if (firstDayOfWeek > 0 && firstDayOfWeek < weekdays.length) {
 +
      weekdays = [].concat(weekdays.splice(firstDayOfWeek, weekdays.length),
 +
          weekdays.splice(0, firstDayOfWeek));
 +
    }
 +
 +
    weekdayContainer.innerHTML = '<tr><th>' + weekdays.join('</th><th>') +
 +
        '</th></tr>';
 +
    calendar.appendChild(weekdayContainer);
 +
  };
 +
 +
  buildDays = function() {
 +
    var firstOfMonth =
 +
        new Date(self.currentYearView, self.currentMonthView, 1).getDay(),
 +
      numDays = date.month.numDays(),
 +
      calendarFragment = document.createDocumentFragment(),
 +
      row = document.createElement('tr'),
 +
      dayCount,
 +
      dayNumber,
 +
      today = '',
 +
      selected = '',
 +
      disabled = '',
 +
      currentTimestamp;
 +
 +
    // Offset the first day by the specified amount
 +
    firstOfMonth -= self.l10n.firstDayOfWeek;
 +
    if (firstOfMonth < 0) {
 +
      firstOfMonth += 7;
 +
    }
 +
 +
    dayCount = firstOfMonth;
 +
    calendarBody.innerHTML = '';
 +
 +
    // Add spacer to line up the first day of the month correctly
 +
    if (firstOfMonth > 0) {
 +
      row.innerHTML += '<td colspan="' + firstOfMonth + '">&nbsp;</td>';
 +
    }
 +
 +
    // Start at 1 since there is no 0th day
 +
    for (dayNumber = 1; dayNumber <= numDays; dayNumber++) {
 +
      // if we have reached the end of a week, wrap to the next line
 +
      if (dayCount === 7) {
 +
        calendarFragment.appendChild(row);
 +
        row = document.createElement('tr');
 +
        dayCount = 0;
 +
      }
 +
 +
      today = isSpecificDay(date.current.day(), date.current.month.integer(),
 +
                            date.current.year(), dayNumber) ? ' today' : '';
 +
      if (self.selectedDate) {
 +
        selected = isSpecificDay(self.selectedDate.day, self.selectedDate.month,
 +
            self.selectedDate.year, dayNumber) ? ' selected' : '';
 +
      }
 +
 +
      if (self.config.minDate || self.config.maxDate) {
 +
        currentTimestamp = new Date(self.currentYearView, self.currentMonthView,
 +
                                    dayNumber).getTime();
 +
        disabled = '';
 +
 +
        if (self.config.minDate && currentTimestamp < self.config.minDate) {
 +
          disabled = ' disabled';
 +
        }
 +
 +
        if (self.config.maxDate && currentTimestamp > self.config.maxDate) {
 +
          disabled = ' disabled';
 +
        }
 +
      }
 +
 +
      row.innerHTML += '<td class="' + today + selected + disabled +
 +
          '"><span class="datepicker-day">' + dayNumber + '</span></td>';
 +
      dayCount++;
 +
    }
 +
 +
    calendarFragment.appendChild(row);
 +
    calendarBody.appendChild(calendarFragment);
 +
  };
 +
 +
  updateNavigationCurrentMonth = function() {
 +
    navigationCurrentMonth.innerHTML = date.month.string() + ' ' +
 +
                                      self.currentYearView;
 +
  };
 +
 +
  buildMonthNavigation = function() {
 +
    var months = document.createElement('div'),
 +
      monthNavigation;
 +
 +
    monthNavigation = '<span class="datepicker-prev-month">&lt;</span>';
 +
    monthNavigation += '<span class="datepicker-next-month">&gt;</span>';
 +
 +
    months.className = 'datepicker-months';
 +
    months.innerHTML = monthNavigation;
 +
 +
    months.appendChild(navigationCurrentMonth);
 +
    updateNavigationCurrentMonth();
 +
    calendarContainer.appendChild(months);
 +
  };
 +
 +
  handleYearChange = function() {
 +
    if (self.currentMonthView < 0) {
 +
      self.currentYearView--;
 +
      self.currentMonthView = 11;
 +
    }
 +
 +
    if (self.currentMonthView > 11) {
 +
      self.currentYearView++;
 +
      self.currentMonthView = 0;
 +
    }
 +
  };
 +
 +
  documentClick = function(event) {
 +
    var parent;
 +
    if (event.target !== self.element && event.target !== wrapperElement) {
 +
      parent = event.target.parentNode;
 +
      if (parent !== wrapperElement) {
 +
        while (parent !== wrapperElement) {
 +
          parent = parent.parentNode;
 +
          if (parent === null) {
 +
            close();
 +
            break;
 +
          }
 +
        }
 +
      }
 +
    }
 +
  };
 +
 +
  calendarClick = function(event) {
 +
    var target = event.target,
 +
      targetClass = target.className,
 +
      currentTimestamp;
 +
 +
    if (targetClass) {
 +
      if (targetClass === 'datepicker-prev-month' ||
 +
          targetClass === 'datepicker-next-month') {
 +
        if (targetClass === 'datepicker-prev-month') {
 +
          self.currentMonthView--;
 +
        } else {
 +
          self.currentMonthView++;
 +
        }
 +
 +
        handleYearChange();
 +
        updateNavigationCurrentMonth();
 +
        buildDays();
 +
      } else if (targetClass === 'datepicker-day' &&
 +
                !self.hasClass(target.parentNode, 'disabled')) {
 +
        self.selectedDate = {
 +
          day: parseInt(target.innerHTML, 10),
 +
          month: self.currentMonthView,
 +
          year: self.currentYearView
 +
        };
 +
 +
        currentTimestamp = new Date(self.currentYearView, self.currentMonthView,
 +
            self.selectedDate.day).getTime();
 +
 +
        if (self.config.altInput) {
 +
          if (self.config.altFormat) {
 +
            self.config.altInput.value = formatDate(self.config.altFormat,
 +
                                                    currentTimestamp);
 +
          } else {
 +
            // Not sure why someone would want to do this... but just in case.
 +
            self.config.altInput.value = formatDate(self.config.dateFormat,
 +
                                                    currentTimestamp);
 +
          }
 +
        }
 +
 +
        self.element.value = formatDate(self.config.dateFormat,
 +
                                        currentTimestamp);
 +
 +
        close();
 +
        buildDays();
 +
      }
 +
    }
 +
  };
 +
 +
  buildCalendar = function() {
 +
    buildMonthNavigation();
 +
    buildWeekdays();
 +
    buildDays();
 +
 +
    calendar.appendChild(calendarBody);
 +
    calendarContainer.appendChild(calendar);
 +
 +
    wrapperElement.appendChild(calendarContainer);
 +
  };
 +
 +
  getOpenEvent = function() {
 +
    if (self.element.nodeName === 'INPUT') {
 +
      return 'focus';
 +
    }
 +
    return 'click';
 +
  };
 +
 +
  bind = function() {
 +
    self.addEventListener(self.element, getOpenEvent(), open);
 +
    self.addEventListener(calendarContainer, 'click', calendarClick);
 +
  };
 +
 +
  open = function() {
 +
    self.addEventListener(document, 'click', documentClick);
 +
    self.addClass(wrapperElement, 'open');
 +
  };
 +
 +
  close = function() {
 +
    self.removeEventListener(document, 'click', documentClick);
 +
    self.removeClass(wrapperElement, 'open');
 +
  };
 +
 +
  destroy = function() {
 +
    var parent,
 +
      element;
 +
 +
    self.removeEventListener(document, 'click', documentClick);
 +
    self.removeEventListener(self.element, getOpenEvent(), open);
 +
 +
    parent = self.element.parentNode;
 +
    parent.removeChild(calendarContainer);
 +
    element = parent.removeChild(self.element);
 +
    parent.parentNode.replaceChild(element, parent);
 +
  };
 +
 +
  init = function() {
 +
    var config,
 +
      parsedDate;
 +
 +
    self.config = {};
 +
    self.destroy = destroy;
 +
 +
    for (config in defaultConfig) {
 +
      self.config[config] = instanceConfig[config] || defaultConfig[config];
 +
    }
 +
 +
    self.element = element;
 +
 +
    if (self.element.value) {
 +
      parsedDate = Date.parse(self.element.value);
 +
    }
 +
 +
    if (parsedDate && !isNaN(parsedDate)) {
 +
      parsedDate = new Date(parsedDate);
 +
      self.selectedDate = {
 +
        day: parsedDate.getDate(),
 +
        month: parsedDate.getMonth(),
 +
        year: parsedDate.getFullYear()
 +
      };
 +
      self.currentYearView = self.selectedDate.year;
 +
      self.currentMonthView = self.selectedDate.month;
 +
      self.currentDayView = self.selectedDate.day;
 +
    } else {
 +
      self.selectedDate = null;
 +
      self.currentYearView = date.current.year();
 +
      self.currentMonthView = date.current.month.integer();
 +
      self.currentDayView = date.current.day();
 +
    }
 +
 +
    wrap();
 +
    buildCalendar();
 +
    bind();
 +
  };
 +
 +
  init();
 +
 +
  return self;
 +
};
 +
 +
DatePicker.init.prototype = {
 +
  hasClass: function(element, className) {
 +
    return element.classList.contains(className);
 +
  },
 +
  addClass: function(element, className) {
 +
    element.classList.add(className);
 +
  },
 +
  removeClass: function(element, className) {
 +
    element.classList.remove(className);
 +
  },
 +
  forEach: function(items, callback) { [].forEach.call(items, callback); },
 +
  querySelectorAll: document.querySelectorAll.bind(document),
 +
  isArray: Array.isArray,
 +
  addEventListener: function(element, type, listener, useCapture) {
 +
    element.addEventListener(type, listener, useCapture);
 +
  },
 +
  removeEventListener: function(element, type, listener, useCapture) {
 +
    element.removeEventListener(type, listener, useCapture);
 +
  },
 +
  l10n: {
 +
    weekdays: {
 +
      shorthand: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
 +
      longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
 +
                'Friday', 'Saturday']
 +
    },
 +
    months: {
 +
      shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
 +
                  'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
 +
      longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
 +
                'August', 'September', 'October', 'November', 'December']
 +
    },
 +
    daysInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
 +
    firstDayOfWeek: 0
 +
  }
 +
};
 +
</syntaxhighlight>
 +
 +
----
 +
 +
<b>datepicker.css:</b>
 +
<syntaxhighlight lang="css">
 +
/* Style guide for the datepicker.js objects */
 +
 +
.datepicker-wrapper {
 +
  display: inline-block;
 +
  position: relative;
 +
}
 +
 +
.datepicker-calendar {
 +
  background-color: #eee;
 +
  border: 1px solid #ddd;
 +
  -moz-border-radius: 4px;
 +
  -webkit-border-radius: 4px;
 +
  border-radius: 4px;
 +
  color: #333;
 +
  display: none;
 +
  font-family: 'Trebuchet MS', Tahoma, Verdana, Arial, sans-serif;
 +
  font-size: 12px;
 +
  left: 0;
 +
  padding: 2px;
 +
  position: absolute;
 +
  top: 100%;
 +
  z-index: 100;
 +
}
 +
 +
.open .datepicker-calendar {
 +
  display: block;
 +
}
 +
 +
.datepicker-calendar .datepicker-months {
 +
  background-color: #4d90fe;  /* Kennedy blue. */
 +
  border: 1px solid #2f5bb7;  /* Dark blue. */
 +
  -moz-border-radius: 4px;
 +
  -webkit-border-radius: 4px;
 +
  border-radius: 4px;
 +
  color: #fff;
 +
  font-size: 120%;
 +
  padding: 2px;
 +
  text-align: center;
 +
}
 +
 +
.datepicker-calendar .datepicker-prev-month,
 +
.datepicker-calendar .datepicker-next-month {
 +
  color: #fff;
 +
  cursor: pointer;
 +
  -moz-border-radius: 4px;
 +
  -webkit-border-radius: 4px;
 +
  border-radius: 4px;
 +
  padding: 0 .4em;
 +
  text-decoration: none;
 +
}
 +
 +
.datepicker-calendar .datepicker-prev-month {
 +
  float: left;
 
}
 
}
  
for (var j = 0; j < 3; j++) { funcs[j](); }     // Log values.
+
.datepicker-calendar .datepicker-next-month {
 +
  float: right;
 +
}
  
// Outputs: 3, 3, 3. // BAD!
+
.datepicker-calendar .datepicker-current-month {
</syntaxhighlight>
+
  padding: 0 .5em;
 +
}
 +
 
 +
.datepicker-calendar .datepicker-prev-month:hover,
 +
.datepicker-calendar .datepicker-next-month:hover {
 +
  background-color: #fdf5ce;
 +
  color: #c77405;
 +
}
  
 +
.datepicker-calendar table {
 +
  border-collapse: collapse;
 +
  padding: 0;
 +
  width: 100%;
 +
}
  
With binding:
+
.datepicker-calendar thead {
 +
  font-size: 90%;
 +
}
  
 +
.datepicker-calendar th,
 +
.datepicker-calendar td {
 +
  width: 14.3%;
 +
}
  
<syntaxhighlight lang="javascript">
+
.datepicker-calendar th {
 +
  text-align: center;
 +
  padding: 5px;
 +
}
  
function log(x) {
+
.datepicker-calendar td {
   console.log('My value: ' + x);
+
   text-align: right;
 +
  padding: 1px;
 
}
 
}
  
var funcs = [];
+
.datepicker-calendar .datepicker-day {
 +
  background-color: #f6f6f6;
 +
  border: 1px solid #ccc;
 +
  color: #1c94c4;
 +
  cursor: pointer;
 +
  display: block;
 +
  padding: 5px;
 +
}
  
for (var i = 0; i < 3; i++) {
+
.datepicker-calendar .datepicker-day:hover {
   funcs[i] = log.bind(this, i);
+
  background-color: #fdf5ce;
 +
  border: 1px solid #fbcb09;
 +
   color: #C77405;
 
}
 
}
  
for (var j = 0; j < 3; j++) { funcs[j](); }     // Log values.
+
.datepicker-calendar .today .datepicker-day {
 +
  background-color: #fff0A5;
 +
  border: 1px solid #fed22f;
 +
  color: #363636;
 +
}
  
// Outputs: 0, 1, 2. // GOOD.
+
.datepicker-calendar .selected .datepicker-day {
 +
  background-color: #1c94c4;
 +
  color: #f6f6f6;
 +
}
 +
 
 +
.datepicker-calendar .disabled .datepicker-day,
 +
.datepicker-calendar .disabled .datepicker-day:hover {
 +
  background-color: #eee;
 +
  border: 1px dotted #ccc;
 +
  color: #bbb;
 +
  cursor: default;
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
 +
{{Acknowledgement|people=<b>Josh Salverda</b> for writing such a brilliant little class - exactly what I was looking for.  :) }}
  
  
Line 49: Line 707:
  
 
* [[JavaScript]] - other JavaScript examples.
 
* [[JavaScript]] - other JavaScript examples.
 +
* [https://github.com/joshsalverda/datepickr Github - datepickr] - Josh Salverda's original code.
  
 
[[Category:Computers]]
 
[[Category:Computers]]
 
[[Category:Programming]]
 
[[Category:Programming]]
 
[[Category:Images]]
 
[[Category:Images]]

Latest revision as of 14:13, 6 February 2019

Date picker demo (datepickerdemo.html) in action.

About

NOTE: This page is a daughter page of: JavaScript


A common task in online forms is to enter a date (often for DOB, but often for reservations), and to help users select the correct date is to have a calendar popup. I found a really fantastic little datepickr class by Josh Salverda (who I've credited below) and uses fairly minimal code: 1 x .js file and 1 x .css file, with ~ 600 total lines (not bad considering all the necessary logic for calendar days etc). I've taken this code, modified it a little, to conform to Google's JavaScript Style Guide and provided it below as a nice base you can copy and paste, and then customize by playing with the styles etc.


There are several large JavaScript libraries which include date pickers, for example JQuery UI's Datepicker, and there are also many other independent date picker scripts such as those listed here.... but I really like Josh's visual style and so this is the one I chose and I'm sure I'll reuse again! :)


NOTE: In HTML5 they have added a new "date" type for the input tag which adds a date picker for you.... however won't work for older browsers. The one line of code is:

<input type="date" name="my_date" id="my_date">

Date Picker Code

INSTRUCTIONS: Create the following three files in the same dir and you should be good to go!

See more involved demo working here.


datepickerdemo.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Date Picker Demo</title>
    <link rel="stylesheet" href="datepicker.css">
  </head>
  <body>
    <h3>Date Picker:</h3>
    <input id="datepick" size="10" class="datepicker" placeholder="mm/dd/yyyy">
    <script src="datepicker.js"></script>
    <script>
      // Custom date format
      DatePicker('.datepicker', { dateFormat: 'm/d/Y'});
    </script>
  </body>
</html>

datepicker.js:

/**
 * @fileoverview Date picker popup calendar widget.
 *
 * CREDIT:
 *   ~ code based on: datepickr 3.0
 *   ~ authored by:   Josh Salverda <josh.salverda@gmail.com>
 *   ~ available at:  https://github.com/joshsalverda/datepickr
 *   ~ licence:       open (http://www.wtfpl.net/)
 *
 * Code was then adjusted by Andrew Noske (andrew.noske@gmail / anoske@)
 * to conform to Google's JavaScript Style Guide.
 */

/**
 * Apply datepicker popup to all do elements matching the selector.
 * @param {Object} selector Selector for DOM elements to apply to
 *     (eg: '.datepicker' - for all elements matching this class).
 * @param {Object} config Configuration values for this
 *     specific instance (eg: '{ dateFormat: 'm/d/Y'}').
 * @constructor
 */
var DatePicker = function(selector, config) {
  'use strict';
  var elements,
    createInstance,
    instances = [],
    i;

  DatePicker.prototype = DatePicker.init.prototype;

  createInstance = function(element) {
    if (element._DatePicker) {
      element._DatePicker.destroy();
    }
    element._DatePicker = new DatePicker.init(element, config);
    return element._DatePicker;
  };

  if (selector.nodeName) {
    return createInstance(selector);
  }

  elements = DatePicker.prototype.querySelectorAll(selector);

  if (elements.length === 1) {
    return createInstance(elements[0]);
  }

  for (i = 0; i < elements.length; i++) {
    instances.push(createInstance(elements[i]));
  }
  return instances;
};

/**
 * Apply datepicker popup to a particular element.
 * @param {Object} element DOM element on which this operates.
 * @param {Object} instanceConfig Configuration values for this
 *     specific instance (eg: '{ dateFormat: 'm/d/Y'}').
 * @constructor
 */
DatePicker.init = function(element, instanceConfig) {
  'use strict';
  var self = this,
    defaultConfig = {
      dateFormat: 'F j, Y',
      altFormat: null,
      altInput: null,
      minDate: null,
      maxDate: null,
      shorthandCurrentMonth: false
    },
    calendarContainer = document.createElement('div'),
    navigationCurrentMonth = document.createElement('span'),
    calendar = document.createElement('table'),
    calendarBody = document.createElement('tbody'),
    wrapperElement,
    currentDate = new Date(),
    wrap,
    date,
    formatDate,
    monthToStr,
    isSpecificDay,
    buildWeekdays,
    buildDays,
    updateNavigationCurrentMonth,
    buildMonthNavigation,
    handleYearChange,
    documentClick,
    calendarClick,
    buildCalendar,
    getOpenEvent,
    bind,
    open,
    close,
    destroy,
    init;

  calendarContainer.className = 'datepicker-calendar';
  navigationCurrentMonth.className = 'datepicker-current-month';
  instanceConfig = instanceConfig || {};

  wrap = function() {
    wrapperElement = document.createElement('div');
    wrapperElement.className = 'datepicker-wrapper';
    self.element.parentNode.insertBefore(wrapperElement, self.element);
    wrapperElement.appendChild(self.element);
  };

  date = {
    current: {
      year: function() {
        return currentDate.getFullYear();
      },
      month: {
        integer: function() {
          return currentDate.getMonth();
        },
        string: function(shorthand) {
          var month = currentDate.getMonth();
          return monthToStr(month, shorthand);
        }
      },
      day: function() {
        return currentDate.getDate();
      }
    },
    month: {
      string: function() {
        return monthToStr(self.currentMonthView,
                          self.config.shorthandCurrentMonth);
      },
      numDays: function() {
        // Checks to see if february is a leap year otherwise
        // return the respective # of days.
        return self.currentMonthView === 1 &&
            (((self.currentYearView % 4 === 0) &&
            (self.currentYearView % 100 !== 0)) ||
            (self.currentYearView % 400 === 0)) ?
            29 : self.l10n.daysInMonth[self.currentMonthView];
      }
    }
  };

  formatDate = function(dateFormat, milliseconds) {
    var formattedDate = '',
      dateObj = new Date(milliseconds),
      formats = {
        d: function() {
          var day = formats.j();
          return (day < 10) ? '0' + day : day;
        },
        D: function() {
          return self.l10n.weekdays.shorthand[formats.w()];
        },
        j: function() {
          return dateObj.getDate();
        },
        l: function() {
          return self.l10n.weekdays.longhand[formats.w()];
        },
        w: function() {
          return dateObj.getDay();
        },
        F: function() {
          return monthToStr(formats.n() - 1, false);
        },
        m: function() {
          var month = formats.n();
          return (month < 10) ? '0' + month : month;
        },
        M: function() {
          return monthToStr(formats.n() - 1, true);
        },
        n: function() {
          return dateObj.getMonth() + 1;
        },
        U: function() {
          return dateObj.getTime() / 1000;
        },
        y: function() {
          return String(formats.Y()).substring(2);
        },
        Y: function() {
          return dateObj.getFullYear();
        }
      },
      formatPieces = dateFormat.split('');

    self.forEach(formatPieces, function(formatPiece, index) {
      if (formats[formatPiece] && formatPieces[index - 1] !== '\\') {
        formattedDate += formats[formatPiece]();
      } else {
        if (formatPiece !== '\\') {
          formattedDate += formatPiece;
        }
      }
    });

    return formattedDate;
  };

  monthToStr = function(date, shorthand) {
    if (shorthand === true) {
      return self.l10n.months.shorthand[date];
    }

    return self.l10n.months.longhand[date];
  };

  isSpecificDay = function(day, month, year, comparison) {
    return day === comparison && self.currentMonthView === month &&
        self.currentYearView === year;
  };

  buildWeekdays = function() {
    var weekdayContainer = document.createElement('thead'),
      firstDayOfWeek = self.l10n.firstDayOfWeek,
      weekdays = self.l10n.weekdays.shorthand;

    if (firstDayOfWeek > 0 && firstDayOfWeek < weekdays.length) {
      weekdays = [].concat(weekdays.splice(firstDayOfWeek, weekdays.length),
          weekdays.splice(0, firstDayOfWeek));
    }

    weekdayContainer.innerHTML = '<tr><th>' + weekdays.join('</th><th>') +
        '</th></tr>';
    calendar.appendChild(weekdayContainer);
  };

  buildDays = function() {
    var firstOfMonth =
        new Date(self.currentYearView, self.currentMonthView, 1).getDay(),
      numDays = date.month.numDays(),
      calendarFragment = document.createDocumentFragment(),
      row = document.createElement('tr'),
      dayCount,
      dayNumber,
      today = '',
      selected = '',
      disabled = '',
      currentTimestamp;

    // Offset the first day by the specified amount
    firstOfMonth -= self.l10n.firstDayOfWeek;
    if (firstOfMonth < 0) {
      firstOfMonth += 7;
    }

    dayCount = firstOfMonth;
    calendarBody.innerHTML = '';

    // Add spacer to line up the first day of the month correctly
    if (firstOfMonth > 0) {
      row.innerHTML += '<td colspan="' + firstOfMonth + '">&nbsp;</td>';
    }

    // Start at 1 since there is no 0th day
    for (dayNumber = 1; dayNumber <= numDays; dayNumber++) {
      // if we have reached the end of a week, wrap to the next line
      if (dayCount === 7) {
        calendarFragment.appendChild(row);
        row = document.createElement('tr');
        dayCount = 0;
      }

      today = isSpecificDay(date.current.day(), date.current.month.integer(),
                            date.current.year(), dayNumber) ? ' today' : '';
      if (self.selectedDate) {
        selected = isSpecificDay(self.selectedDate.day, self.selectedDate.month,
            self.selectedDate.year, dayNumber) ? ' selected' : '';
      }

      if (self.config.minDate || self.config.maxDate) {
        currentTimestamp = new Date(self.currentYearView, self.currentMonthView,
                                    dayNumber).getTime();
        disabled = '';

        if (self.config.minDate && currentTimestamp < self.config.minDate) {
          disabled = ' disabled';
        }

        if (self.config.maxDate && currentTimestamp > self.config.maxDate) {
          disabled = ' disabled';
        }
      }

      row.innerHTML += '<td class="' + today + selected + disabled +
          '"><span class="datepicker-day">' + dayNumber + '</span></td>';
      dayCount++;
    }

    calendarFragment.appendChild(row);
    calendarBody.appendChild(calendarFragment);
  };

  updateNavigationCurrentMonth = function() {
    navigationCurrentMonth.innerHTML = date.month.string() + ' ' +
                                       self.currentYearView;
  };

  buildMonthNavigation = function() {
    var months = document.createElement('div'),
      monthNavigation;

    monthNavigation = '<span class="datepicker-prev-month">&lt;</span>';
    monthNavigation += '<span class="datepicker-next-month">&gt;</span>';

    months.className = 'datepicker-months';
    months.innerHTML = monthNavigation;

    months.appendChild(navigationCurrentMonth);
    updateNavigationCurrentMonth();
    calendarContainer.appendChild(months);
  };

  handleYearChange = function() {
    if (self.currentMonthView < 0) {
      self.currentYearView--;
      self.currentMonthView = 11;
    }

    if (self.currentMonthView > 11) {
      self.currentYearView++;
      self.currentMonthView = 0;
    }
  };

  documentClick = function(event) {
    var parent;
    if (event.target !== self.element && event.target !== wrapperElement) {
      parent = event.target.parentNode;
      if (parent !== wrapperElement) {
        while (parent !== wrapperElement) {
          parent = parent.parentNode;
          if (parent === null) {
            close();
            break;
          }
        }
      }
    }
  };

  calendarClick = function(event) {
    var target = event.target,
      targetClass = target.className,
      currentTimestamp;

    if (targetClass) {
      if (targetClass === 'datepicker-prev-month' ||
          targetClass === 'datepicker-next-month') {
        if (targetClass === 'datepicker-prev-month') {
          self.currentMonthView--;
        } else {
          self.currentMonthView++;
        }

        handleYearChange();
        updateNavigationCurrentMonth();
        buildDays();
      } else if (targetClass === 'datepicker-day' &&
                 !self.hasClass(target.parentNode, 'disabled')) {
        self.selectedDate = {
          day: parseInt(target.innerHTML, 10),
          month: self.currentMonthView,
          year: self.currentYearView
        };

        currentTimestamp = new Date(self.currentYearView, self.currentMonthView,
            self.selectedDate.day).getTime();

        if (self.config.altInput) {
          if (self.config.altFormat) {
            self.config.altInput.value = formatDate(self.config.altFormat,
                                                    currentTimestamp);
          } else {
            // Not sure why someone would want to do this... but just in case.
            self.config.altInput.value = formatDate(self.config.dateFormat,
                                                    currentTimestamp);
          }
        }

        self.element.value = formatDate(self.config.dateFormat,
                                        currentTimestamp);

        close();
        buildDays();
      }
    }
  };

  buildCalendar = function() {
    buildMonthNavigation();
    buildWeekdays();
    buildDays();

    calendar.appendChild(calendarBody);
    calendarContainer.appendChild(calendar);

    wrapperElement.appendChild(calendarContainer);
  };

  getOpenEvent = function() {
    if (self.element.nodeName === 'INPUT') {
      return 'focus';
    }
    return 'click';
  };

  bind = function() {
    self.addEventListener(self.element, getOpenEvent(), open);
    self.addEventListener(calendarContainer, 'click', calendarClick);
  };

  open = function() {
    self.addEventListener(document, 'click', documentClick);
    self.addClass(wrapperElement, 'open');
  };

  close = function() {
    self.removeEventListener(document, 'click', documentClick);
    self.removeClass(wrapperElement, 'open');
  };

  destroy = function() {
    var parent,
      element;

    self.removeEventListener(document, 'click', documentClick);
    self.removeEventListener(self.element, getOpenEvent(), open);

    parent = self.element.parentNode;
    parent.removeChild(calendarContainer);
    element = parent.removeChild(self.element);
    parent.parentNode.replaceChild(element, parent);
  };

  init = function() {
    var config,
      parsedDate;

    self.config = {};
    self.destroy = destroy;

    for (config in defaultConfig) {
      self.config[config] = instanceConfig[config] || defaultConfig[config];
    }

    self.element = element;

    if (self.element.value) {
      parsedDate = Date.parse(self.element.value);
    }

    if (parsedDate && !isNaN(parsedDate)) {
      parsedDate = new Date(parsedDate);
      self.selectedDate = {
        day: parsedDate.getDate(),
        month: parsedDate.getMonth(),
        year: parsedDate.getFullYear()
      };
      self.currentYearView = self.selectedDate.year;
      self.currentMonthView = self.selectedDate.month;
      self.currentDayView = self.selectedDate.day;
    } else {
      self.selectedDate = null;
      self.currentYearView = date.current.year();
      self.currentMonthView = date.current.month.integer();
      self.currentDayView = date.current.day();
    }

    wrap();
    buildCalendar();
    bind();
  };

  init();

  return self;
};

DatePicker.init.prototype = {
  hasClass: function(element, className) {
    return element.classList.contains(className);
  },
  addClass: function(element, className) {
    element.classList.add(className);
  },
  removeClass: function(element, className) {
    element.classList.remove(className);
  },
  forEach: function(items, callback) { [].forEach.call(items, callback); },
  querySelectorAll: document.querySelectorAll.bind(document),
  isArray: Array.isArray,
  addEventListener: function(element, type, listener, useCapture) {
    element.addEventListener(type, listener, useCapture);
  },
  removeEventListener: function(element, type, listener, useCapture) {
    element.removeEventListener(type, listener, useCapture);
  },
  l10n: {
    weekdays: {
      shorthand: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
      longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
                 'Friday', 'Saturday']
    },
    months: {
      shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
                  'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
      longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
                 'August', 'September', 'October', 'November', 'December']
    },
    daysInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
    firstDayOfWeek: 0
  }
};

datepicker.css:

/* Style guide for the datepicker.js objects */

.datepicker-wrapper {
  display: inline-block;
  position: relative;
}

.datepicker-calendar {
  background-color: #eee;
  border: 1px solid #ddd;
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  border-radius: 4px;
  color: #333;
  display: none;
  font-family: 'Trebuchet MS', Tahoma, Verdana, Arial, sans-serif;
  font-size: 12px;
  left: 0;
  padding: 2px;
  position: absolute;
  top: 100%;
  z-index: 100;
}

.open .datepicker-calendar {
  display: block;
}

.datepicker-calendar .datepicker-months {
  background-color: #4d90fe;  /* Kennedy blue. */
  border: 1px solid #2f5bb7;  /* Dark blue. */
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  border-radius: 4px;
  color: #fff;
  font-size: 120%;
  padding: 2px;
  text-align: center;
}

.datepicker-calendar .datepicker-prev-month,
.datepicker-calendar .datepicker-next-month {
  color: #fff;
  cursor: pointer;
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  border-radius: 4px;
  padding: 0 .4em;
  text-decoration: none;
}

.datepicker-calendar .datepicker-prev-month {
  float: left;
}

.datepicker-calendar .datepicker-next-month {
  float: right;
}

.datepicker-calendar .datepicker-current-month {
  padding: 0 .5em;
}

.datepicker-calendar .datepicker-prev-month:hover,
.datepicker-calendar .datepicker-next-month:hover {
  background-color: #fdf5ce;
  color: #c77405;
}

.datepicker-calendar table {
  border-collapse: collapse;
  padding: 0;
  width: 100%;
}

.datepicker-calendar thead {
  font-size: 90%;
}

.datepicker-calendar th,
.datepicker-calendar td {
  width: 14.3%;
}

.datepicker-calendar th {
  text-align: center;
  padding: 5px;
}

.datepicker-calendar td {
  text-align: right;
  padding: 1px;
}

.datepicker-calendar .datepicker-day {
  background-color: #f6f6f6;
  border: 1px solid #ccc;
  color: #1c94c4;
  cursor: pointer;
  display: block;
  padding: 5px;
}

.datepicker-calendar .datepicker-day:hover {
  background-color: #fdf5ce;
  border: 1px solid #fbcb09;
  color: #C77405;
}

.datepicker-calendar .today .datepicker-day {
  background-color: #fff0A5;
  border: 1px solid #fed22f;
  color: #363636;
}

.datepicker-calendar .selected .datepicker-day {
  background-color: #1c94c4;
  color: #f6f6f6;
}

.datepicker-calendar .disabled .datepicker-day,
.datepicker-calendar .disabled .datepicker-day:hover {
  background-color: #eee;
  border: 1px dotted #ccc;
  color: #bbb;
  cursor: default;
}


Acknowledgements: Josh Salverda for writing such a brilliant little class - exactly what I was looking for. :)


See Also