617 lines
20 KiB
JavaScript
617 lines
20 KiB
JavaScript
;(function ($, window, document, undefined) {
|
|
|
|
"use strict";
|
|
|
|
// Create the defaults once
|
|
var pluginName = "dateDropdowns",
|
|
pluginDefaults = {
|
|
defaultDate: null,
|
|
defaultDateFormat: "yyyy-mm-dd",
|
|
displayFormat: "dmy",
|
|
submitFormat: "yyyy-mm-dd",
|
|
minAge: 0,
|
|
maxAge: 120,
|
|
minYear: null,
|
|
maxYear: null,
|
|
submitFieldName: "date",
|
|
wrapperClass: "date-dropdowns",
|
|
dropdownClass: null,
|
|
daySuffixes: true,
|
|
monthSuffixes: true,
|
|
monthFormat: "long",
|
|
required: false
|
|
};
|
|
|
|
// The actual plugin constructor
|
|
function Plugin (element, options) {
|
|
this.element = element; // Element handle
|
|
this.$element = $(element); // jQuery element handle
|
|
this.config = $.extend({}, pluginDefaults, options); // Plugin options
|
|
this.internals = { // Internal variables
|
|
objectRefs: {}
|
|
};
|
|
this._defaults = pluginDefaults; // Reference to the plugin defaults
|
|
this._name = pluginName; // Reference to the plugin name
|
|
this.init();
|
|
|
|
return this;
|
|
}
|
|
|
|
Plugin.message = {
|
|
day: "Day",
|
|
month: "Month",
|
|
year: "Year"
|
|
};
|
|
|
|
// Avoid Plugin.prototype conflicts
|
|
$.extend(Plugin.prototype, {
|
|
|
|
/**
|
|
* Initialise the plugin
|
|
*/
|
|
init: function () {
|
|
this.checkForDuplicateElement();
|
|
this.setInternalVariables();
|
|
this.setupMarkup();
|
|
this.buildDropdowns();
|
|
this.attachDropdowns();
|
|
this.bindChangeEvent();
|
|
|
|
if (this.config.defaultDate) {
|
|
this.populateDefaultDate();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check whether an element exists with the same name attribute. If so, throw an error
|
|
*/
|
|
checkForDuplicateElement: function() {
|
|
if ($("input[name=\"" + this.config.submitFieldName + "\"]").length) {
|
|
$.error("Duplicate element found");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Set the plugin"s internal variables
|
|
*/
|
|
setInternalVariables: function() {
|
|
var date = new Date();
|
|
this.internals.currentDay = date.getDate();
|
|
this.internals.currentMonth = date.getMonth() + 1;
|
|
this.internals.currentYear = date.getFullYear();
|
|
this.internals.monthShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
this.internals.monthLong = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
},
|
|
|
|
/**
|
|
* Set the container which will hold the date dropdowns.
|
|
*
|
|
* - If the element on which the plugin was called is an input of type text or hidden, set it to
|
|
* be hidden and wrap it in a div. This outer div will become the container.
|
|
* - If the element was an HTML element (e.g. <div/>), create a hidden text field within it, and use
|
|
* the div as the container.
|
|
*/
|
|
setupMarkup: function() {
|
|
var wrapper, hiddenField;
|
|
|
|
if (this.element.tagName.toLowerCase() === "input") {
|
|
if (!this.config.defaultDate) {
|
|
this.config.defaultDate = this.element.value;
|
|
}
|
|
|
|
// Configure the input element and wrap
|
|
hiddenField = this.$element.attr("type", "hidden")
|
|
.attr("name", this.config.submitFieldName)
|
|
.wrap("<div class=\"" + this.config.wrapperClass + "\"></div>");
|
|
|
|
wrapper = this.$element.parent();
|
|
|
|
} else {
|
|
|
|
// Build a hidden input and set this.$element as the wrapper
|
|
hiddenField = $("<input/>", {
|
|
type: "hidden",
|
|
name: this.config.submitFieldName
|
|
});
|
|
|
|
this.$element.append(hiddenField).addClass(this.config.wrapperClass);
|
|
|
|
wrapper = this.$element;
|
|
}
|
|
|
|
// Store a reference to the wrapper and hidden field elements for later use
|
|
this.internals.objectRefs.pluginWrapper = wrapper;
|
|
this.internals.objectRefs.hiddenField = hiddenField;
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Generate the Day, Month and Year dropdowns
|
|
*/
|
|
buildDropdowns: function() {
|
|
var $dayDropdown, $monthDropdown, $yearDropdown;
|
|
|
|
// Build the day dropdown element
|
|
$dayDropdown = this.buildDayDropdown();
|
|
this.internals.objectRefs.dayDropdown = $dayDropdown;
|
|
|
|
$monthDropdown = this.buildMonthDropdown();
|
|
this.internals.objectRefs.monthDropdown = $monthDropdown;
|
|
|
|
$yearDropdown = this.buildYearDropdown();
|
|
this.internals.objectRefs.yearDropdown = $yearDropdown;
|
|
|
|
return true;
|
|
|
|
//this.internals.objectRefs.pluginWrapper.append($dayDropdown)
|
|
// .append($monthDropdown)
|
|
// .append($yearDropdown);
|
|
},
|
|
|
|
/**
|
|
* Attach the generated dropdowns to the container
|
|
*/
|
|
attachDropdowns: function() {
|
|
var $element = this.internals.objectRefs.pluginWrapper,
|
|
$daySelect = this.internals.objectRefs.dayDropdown,
|
|
$monthSelect = this.internals.objectRefs.monthDropdown,
|
|
$yearSelect = this.internals.objectRefs.yearDropdown;
|
|
|
|
switch (this.config.displayFormat) {
|
|
case "mdy":
|
|
$element.append($monthSelect, $daySelect, $yearSelect);
|
|
break;
|
|
case "ymd":
|
|
$element.append($yearSelect, $monthSelect, $daySelect);
|
|
break;
|
|
case "dmy":
|
|
default:
|
|
$element.append($daySelect, $monthSelect, $yearSelect);
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Bind the change event to the generated dropdowns
|
|
*/
|
|
bindChangeEvent: function() {
|
|
var $daySelect = this.internals.objectRefs.dayDropdown,
|
|
$monthSelect = this.internals.objectRefs.monthDropdown,
|
|
$yearSelect = this.internals.objectRefs.yearDropdown,
|
|
pluginHandle = this,
|
|
objectRefs = this.internals.objectRefs;
|
|
|
|
objectRefs.pluginWrapper.on("change", "select", function() {
|
|
var day = $daySelect.val(),
|
|
month = $monthSelect.val(),
|
|
year = $yearSelect.val(),
|
|
invalidDate = true,
|
|
newDate;
|
|
|
|
// Find out whether the change has made the date invalid (e.g. 31st Feb)
|
|
invalidDate = pluginHandle.checkDate(day, month, year);
|
|
|
|
// If invalid - add an error class to the day dropdown and return
|
|
if (invalidDate) {
|
|
objectRefs.dayDropdown.addClass("invalid");
|
|
return false;
|
|
}
|
|
|
|
if ("00" !== objectRefs.dayDropdown.val()) {
|
|
objectRefs.dayDropdown.removeClass("invalid");
|
|
}
|
|
|
|
// Empty the hidden field after each change
|
|
objectRefs.hiddenField.val("");
|
|
|
|
// Only format the submit date if a full date has been selected
|
|
if (!invalidDate && (day * month * year !== 0)) {
|
|
newDate = pluginHandle.formatSubmitDate(day, month, year);
|
|
|
|
objectRefs.hiddenField.val(newDate);
|
|
}
|
|
|
|
objectRefs.hiddenField.change();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Take a provided default date and populate both the hidden field and the
|
|
* dropdown elements with the relevant formatted values
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
populateDefaultDate: function() {
|
|
var date = this.config.defaultDate,
|
|
parts = [],
|
|
day = "",
|
|
month = "",
|
|
year = "";
|
|
|
|
switch (this.config.defaultDateFormat) {
|
|
case "yyyy-mm-dd":
|
|
default:
|
|
parts = date.split("-");
|
|
day = parts[2];
|
|
month = parts[1];
|
|
year = parts[0];
|
|
break;
|
|
|
|
case "dd/mm/yyyy":
|
|
parts = date.split("/");
|
|
day = parts[0];
|
|
month = parts[1];
|
|
year = parts[2];
|
|
break;
|
|
|
|
case "mm/dd/yyyy":
|
|
parts = date.split("/");
|
|
day = parts[1];
|
|
month = parts[0];
|
|
year = parts[2];
|
|
break;
|
|
|
|
case "unix":
|
|
parts = new Date();
|
|
parts.setTime(date * 1000);
|
|
day = parts.getDate() + "";
|
|
month = (parts.getMonth() + 1) + "";
|
|
year = parts.getFullYear();
|
|
|
|
if (day.length < 2) {
|
|
day = "0" + day;
|
|
}
|
|
if (month.length < 2) {
|
|
month = "0" + month;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Set the values on the dropdowns
|
|
this.internals.objectRefs.dayDropdown.val(day);
|
|
this.internals.objectRefs.monthDropdown.val(month);
|
|
this.internals.objectRefs.yearDropdown.val(year);
|
|
this.internals.objectRefs.hiddenField.val(date);
|
|
|
|
if (true === this.checkDate(day, month, year)) {
|
|
this.internals.objectRefs.dayDropdown.addClass("invalid");
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Build a generic dropdown element
|
|
*
|
|
* @param type
|
|
* @returns {*|HTMLElement}
|
|
*/
|
|
buildBaseDropdown: function(type) {
|
|
var classString = type;
|
|
|
|
if (this.config.dropdownClass) {
|
|
classString += " " + this.config.dropdownClass;
|
|
}
|
|
|
|
return $("<select></select>", {
|
|
class: classString,
|
|
name: this.config.submitFieldName + "_[" + type + "]",
|
|
required: this.config.required
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Build the day dropdown element
|
|
*
|
|
* @returns {*|HTMLElement}
|
|
*/
|
|
buildDayDropdown: function() {
|
|
var day,
|
|
dropdown = this.buildBaseDropdown("day"),
|
|
option = document.createElement("option");
|
|
|
|
option.setAttribute("value", "");
|
|
option.appendChild(document.createTextNode(Plugin.message.day));
|
|
dropdown.append(option);
|
|
|
|
// Days 1-9
|
|
for (var i = 1; i < 10; i++) {
|
|
if (this.config.daySuffixes) {
|
|
day = i + this.getSuffix(i);
|
|
} else {
|
|
day = "0" + i;
|
|
}
|
|
option = document.createElement("option");
|
|
option.setAttribute("value", "0" + i);
|
|
option.appendChild(document.createTextNode(day));
|
|
dropdown.append(option);
|
|
}
|
|
|
|
// Days 10-31
|
|
for (var j = 10; j <= 31; j++) {
|
|
day = j;
|
|
|
|
if (this.config.daySuffixes) {
|
|
day = j + this.getSuffix(j);
|
|
}
|
|
option = document.createElement("option");
|
|
option.setAttribute("value", j);
|
|
option.appendChild(document.createTextNode(day));
|
|
dropdown.append(option);
|
|
}
|
|
|
|
return dropdown;
|
|
},
|
|
|
|
/**
|
|
* Build the month dropdown element
|
|
*
|
|
* @returns {*|HTMLElement}
|
|
*/
|
|
buildMonthDropdown: function() {
|
|
var dropdown = this.buildBaseDropdown("month"),
|
|
option = document.createElement("option");
|
|
|
|
option.setAttribute("value", "");
|
|
option.appendChild(document.createTextNode(Plugin.message.month));
|
|
dropdown.append(option);
|
|
|
|
// Populate the month values
|
|
for (var monthNo = 1; monthNo <= 12; monthNo++) {
|
|
|
|
var month;
|
|
|
|
switch (this.config.monthFormat) {
|
|
case "short":
|
|
month = this.internals.monthShort[monthNo - 1];
|
|
break;
|
|
case "long":
|
|
month = this.internals.monthLong[monthNo - 1];
|
|
break;
|
|
case "numeric":
|
|
month = monthNo;
|
|
|
|
if (this.config.monthSuffixes) {
|
|
month += this.getSuffix(monthNo);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (monthNo < 10) {
|
|
monthNo = "0" + monthNo;
|
|
}
|
|
|
|
option = document.createElement("option");
|
|
option.setAttribute("value", monthNo);
|
|
option.appendChild(document.createTextNode(month));
|
|
dropdown.append(option);
|
|
}
|
|
|
|
return dropdown;
|
|
},
|
|
|
|
/**
|
|
* Build the year dropdown element.
|
|
*
|
|
* By default minYear and maxYear are null, however if provided they take precedence over
|
|
* the minAge and maxAge values.
|
|
*
|
|
* @returns {*|HTMLElement}
|
|
*/
|
|
buildYearDropdown: function() {
|
|
var minYear = this.config.minYear,
|
|
maxYear = this.config.maxYear,
|
|
dropdown = this.buildBaseDropdown("year"),
|
|
option = document.createElement("option");
|
|
|
|
option.setAttribute("value", "");
|
|
option.appendChild(document.createTextNode(Plugin.message.year));
|
|
dropdown.append(option);
|
|
|
|
if (!minYear) {
|
|
minYear = this.internals.currentYear - (this.config.maxAge + 1);
|
|
}
|
|
|
|
if (!maxYear) {
|
|
maxYear = this.internals.currentYear - this.config.minAge;
|
|
}
|
|
|
|
for (var i = maxYear; i >= minYear; i--) {
|
|
option = document.createElement("option");
|
|
option.setAttribute("value", i);
|
|
option.appendChild(document.createTextNode(i));
|
|
dropdown.append(option);
|
|
}
|
|
|
|
return dropdown;
|
|
},
|
|
|
|
/**
|
|
* Get the relevant suffix for a day/month number
|
|
*
|
|
* @param number
|
|
* @returns {string}
|
|
*/
|
|
getSuffix: function(number) {
|
|
var suffix = "";
|
|
|
|
switch (number % 10){
|
|
case 1:
|
|
suffix = (number % 100 === 11) ? "th" : "st";
|
|
break;
|
|
case 2:
|
|
suffix = (number % 100 === 12) ? "th" : "nd";
|
|
break;
|
|
case 3:
|
|
suffix = (number % 100 === 13) ? "th" : "rd";
|
|
break;
|
|
default:
|
|
suffix = "th";
|
|
break;
|
|
}
|
|
|
|
return suffix;
|
|
},
|
|
|
|
/**
|
|
* Check whether the date entered is invalid, e.g. 31st Feb
|
|
*
|
|
* @param day
|
|
* @param month
|
|
* @param year
|
|
* @returns {boolean}
|
|
*/
|
|
checkDate: function(day, month, year) {
|
|
var invalidDate;
|
|
|
|
if (month !== "00") {
|
|
var numDaysInMonth = (new Date(year, month, 0).getDate()),
|
|
selectedDayInt = parseInt(day, 10);
|
|
|
|
invalidDate = this.updateDayOptions(numDaysInMonth, selectedDayInt);
|
|
|
|
// If the date is invalid, empty the hidden field to prevent invalid submissions
|
|
if (invalidDate) {
|
|
this.internals.objectRefs.hiddenField.val("");
|
|
}
|
|
}
|
|
|
|
return invalidDate;
|
|
},
|
|
|
|
/**
|
|
* Update the day options available based on the month now selected
|
|
*
|
|
* @param numDaysInMonth
|
|
* @param selectedDayInt
|
|
* @returns {boolean}
|
|
*/
|
|
updateDayOptions: function(numDaysInMonth, selectedDayInt) {
|
|
var lastDayOption = parseInt(this.internals.objectRefs.dayDropdown.children(":last").val(), 10),
|
|
newDayOption = "",
|
|
newDayText = "",
|
|
invalidDay = false;
|
|
|
|
// If the selected month has less days than the Day dropdown currently contains - remove the extra days
|
|
if (lastDayOption > numDaysInMonth) {
|
|
|
|
while (lastDayOption > numDaysInMonth) {
|
|
this.internals.objectRefs.dayDropdown.children(":last").remove();
|
|
lastDayOption--;
|
|
}
|
|
|
|
// If the user-selected day is removed, indicate this so a message can be shown to the user
|
|
if (selectedDayInt > numDaysInMonth) {
|
|
invalidDay = true;
|
|
}
|
|
|
|
// If the month contains more days than the Day dropdown contains - add the missing options
|
|
} else if (lastDayOption < numDaysInMonth) {
|
|
|
|
while (lastDayOption < numDaysInMonth) {
|
|
|
|
newDayOption = ++lastDayOption;
|
|
newDayText = newDayOption;
|
|
|
|
// Add the suffix if required
|
|
if (this.config.daySuffixes) {
|
|
newDayText += this.getSuffix(lastDayOption);
|
|
}
|
|
|
|
// Build the option and append to the dropdown
|
|
var option = document.createElement("option");
|
|
option.setAttribute("value", newDayOption);
|
|
option.appendChild(document.createTextNode(newDayText));
|
|
this.internals.objectRefs.dayDropdown.append(option);
|
|
}
|
|
}
|
|
|
|
return invalidDay;
|
|
},
|
|
|
|
/**
|
|
* Format the selected date based on the submitFormat config value provided
|
|
*
|
|
* @param day
|
|
* @param month
|
|
* @param year
|
|
* @returns {*}
|
|
*/
|
|
formatSubmitDate: function(day, month, year) {
|
|
var formattedDate,
|
|
_date;
|
|
|
|
switch (this.config.submitFormat) {
|
|
case "yyyy-mm-dd":
|
|
default:
|
|
formattedDate = year + "-" + month + "-" + day;
|
|
break;
|
|
|
|
case "mm/dd/yyyy":
|
|
formattedDate = month + "/" + day + "/" + year;
|
|
break;
|
|
|
|
case "dd/mm/yyyy":
|
|
formattedDate = day + "/" + month + "/" + year;
|
|
break;
|
|
|
|
case "unix":
|
|
_date = new Date();
|
|
_date.setDate(day);
|
|
_date.setMonth(month - 1);
|
|
_date.setYear(year);
|
|
formattedDate = Math.round(_date.getTime() / 1000);
|
|
}
|
|
|
|
return formattedDate;
|
|
},
|
|
|
|
/**
|
|
* Revert the changes applied by the plugin on the specified element
|
|
*/
|
|
destroy: function() {
|
|
var wrapperClass = this.config.wrapperClass;
|
|
|
|
if (this.$element.hasClass(wrapperClass)) {
|
|
this.$element.empty();
|
|
} else {
|
|
var $parent = this.$element.parent(),
|
|
$select = $parent.find("select");
|
|
|
|
this.$element.unwrap();
|
|
$select.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
// A really lightweight plugin wrapper around the constructor,
|
|
// preventing against multiple instantiations
|
|
$.fn[ pluginName ] = function ( options ) {
|
|
this.each(function() {
|
|
if(typeof options === "string") {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
var plugin = $.data(this, "plugin_" + pluginName);
|
|
|
|
if (typeof plugin === "undefined") {
|
|
$.error("Please initialize the plugin before calling this method.");
|
|
return false;
|
|
}
|
|
plugin[options].apply(plugin, args);
|
|
} else {
|
|
if ( !$.data( this, "plugin_" + pluginName ) ) {
|
|
$.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
|
|
}
|
|
}
|
|
});
|
|
// chain jQuery functions
|
|
return this;
|
|
};
|
|
|
|
})( jQuery, window, document );
|