// TinyDatePicker was written as an experiment to see how small a functional date picker // utility could be. Procedural is a minification optimization. function TinyDatePicker(input, options) { 'use strict'; ///////////////////////////////////////////////////////// // Initialization and state variables var opts = initializeOptions(options); // The current selection (not necessarily confirmed by the user) var currentDate = opts.parse(input.value); var minStamp = opts.min && new Date(opts.min).getTime(); var maxStamp = opts.max && new Date(opts.max).getTime(); var initialInRange = inRange(currentDate); if (!initialInRange) { currentDate = (opts.min) ? opts.parse(opts.min) : opts.parse(opts.max); input.value && (input.value = opts.format(currentDate)); } // The current date in the associated input var currentValue = currentDate; var el = buildCalendarElement(currentDate, opts); var isHiding = false; // Used to prevent the calendar from showing when transitioning to hidden var focusCatcher = htmlToElement(''); var body = document.body; var CustomEvent = window.CustomEvent; input.readOnly = true; if (!inRange(currentDate)) { setDate(opts.parse(opts.min ? opts.min : opts.max)); input.value = opts.format(currentDate); } ///////////////////////////////////////////////////////// // Unintrusive polyfill the custom event for IE9+ (function () { if (typeof CustomEvent === 'function') return false; CustomEvent = function (event, params) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; } CustomEvent.prototype = window.Event.prototype; })(); ///////////////////////////////////////////////////////// // Event handling/state management on(input, 'focus', show); on(input, 'click', show); on(el, 'keydown', mapKeys({ '37': shiftDay(-1), // Left '38': shiftDay(-7), // Up '39': shiftDay(1), // Right '40': shiftDay(7), // Down '13': function () { pickDate(currentDate) }, // Enter, '27': hide // Esc })); on(el, 'click', mapClick({ 'dp-clear': function () { pickDate() }, 'dp-close': hide, 'dp-wrapper': hide, 'dp-prev': shiftMonth(-1), 'dp-next': shiftMonth(1), 'dp-today': function () { pickDate(new Date()) }, 'dp-day': function (e) { var time = e.target.getAttribute('data-dp'); time && (pickDate(new Date(parseInt(time)))); }, })); on(el, 'mousedown', function (e) { e.preventDefault(); // Prevent loss of focus }); on(el, 'blur', function (e) { setTimeout(function () { el.contains(document.activeElement) || hide(); }, 1); }); ///////////////////////////////////////////////////////// // UI manipulation functions function show() { if (isHiding) return; setDate(opts.parse(input.value)); body.appendChild(el); body.appendChild(focusCatcher); setTimeout(function () { el.className += ' dp-visible'; focus(); }, 1); } function hide() { if (!body.contains(el)) return; isHiding = 1; input.focus(); input.selectionEnd = input.selectionStart; body.removeChild(el); body.removeChild(focusCatcher); el.className = el.className.replace(' dp-visible', ''); setTimeout(function () { isHiding = 0 }, 10); } function redraw() { el.innerHTML = buildCalendarElement(currentDate, opts).innerHTML; focus(); } function focus() { el.querySelector('.dp-selected').focus(); } ///////////////////////////////////////////////////////// // Date manipulation functions function pickDate(date) { if (date && !inRange(date)) { return; } input.value = date ? opts.format(date) : ''; currentValue = date || currentDate; setDate(date); hide(); // Make sure the input fires its change event input.dispatchEvent(new CustomEvent('change', { bubbles: true })); } function setDate(date) { if (date) { currentDate = date; redraw(); } } function shiftDay(amount) { return function () { currentDate.setDate(currentDate.getDate() + amount); setDate(currentDate); } } function shiftMonth(direction) { return function () { var dt = new Date(currentDate); dt.setDate(1); if (direction > 0) { dt.setMonth(dt.getMonth() + 2); } dt.setDate(dt.getDate() - 1); if (currentDate.getDate() < dt.getDate()) { dt.setDate(currentDate.getDate()); } setDate(dt); } } function inRange(dateOrString) { var date = (typeof dateOrString == 'String') ? new Date(dateOrString) : dateOrString; var stamp = date ? date.getTime() : Date.now(); return (!minStamp || stamp >= minStamp) && (!maxStamp || stamp <= maxStamp); } ///////////////////////////////////////////////////////// // Event mapping helpers function mapKeys(map) { return function (e) { var action = map[e.which]; if (action && /dp-selected/.test(e.target.className)) { e.preventDefault(); action(); } } } function mapClick(map) { return function (e) { e.target.className.split(/[\s]+/g).forEach(function (key) { map[key] && map[key](e); }); } } ///////////////////////////////////////////////////////// // Default options function initializeOptions(opts) { return mergeObj({ format: function (date) { return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear(); }, parse: function (str) { var date = new Date(str); return isNaN(date) ? new Date() : date; }, days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], today: 'Today', clear: 'Clear', close: 'Close', min: null, max: null, weekStartsMonday: false }, opts); } ///////////////////////////////////////////////////////// // Helper rendering functions function buildCalendarElement(date, opts) { return htmlToElement('