// Beginning of module definition ///////////////////////////////////////////////////////////////// var TinyDatePicker = (function() { // Indenting all the way over, since the rest of the source is really one module 'use strict'; // Constants... var left = 37; var up = 38; var right = 39; var down = 40; var CustomEvent = getCustomEventConstructor(); // The module export... return TinyDatePicker; // Constructs a new instance of the tiny date picker function TinyDatePicker(input, opts) { var context = buildContext(input, opts || {}); if (context.isModal) { input.readOnly = true; } else { // For the dropdown calendar, we need to hide when the input loses focus // for the modal, we never do this. on('blur', input, buffer(5, function () { if (context.el && !context.el.contains(document.activeElement)) { hideCalendar(context); } })); } on('click', input, function () { showCalendar(context); }); // With the modal, we always begin and end by setting focus to the input // so that tabbing works as expected. This means the focus event needs // to be smart. With the dropdown, we only ever show on focus. on('focus', input, buffer(5, function () { if (context.isModal && isShowing(context)) { hideCalendar(context); } else { showCalendar(context); } })); } // Builds the date picker's settings based on the opts provided. function buildContext(input, opts) { var context = { input: input, mode: opts.mode || 'dp-modal', days: opts.days || ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], months: opts.months || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], today: opts.today || 'Today', clear: opts.clear || 'Clear', close: opts.close || 'Close', format: opts.format || function (date) { return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear(); }, parse: opts.parse || function (str) { var date = new Date(str); return isNaN(date) ? now() : date; }, onChange: function (date) { if (date && !inRange(context, date)) { return; } if (date) { context.selectedDate = new Date(context.currentDate = date); } input.value = date ? context.format(date) : ''; // In modal-mode, if we are setting the value, // we are hiding. if (context.isModal) { input.focus(); } else { render(calHtml, context); } input.dispatchEvent(new CustomEvent('change', {bubbles: true})); }, weekStartsMonday: opts.weekStartsMonday, }; context.min = initMinMax(context, opts.min, -100); context.max = initMinMax(context, opts.max, 100); context.isModal = context.mode === 'dp-modal'; return context; } // Buffers calls to fn so they only happen once in ms milliseconds function buffer(ms, fn) { var timeout = undefined; return function () { clearTimeout(timeout); timeout = setTimeout(fn, ms); } } function showCalendar(context) { if (context.el) { return; } var input = context.input; var el = document.createElement('div'); el.className = context.mode; // dp-focuser allows us to capture the tab event // and put the focus back where it belongs, el.innerHTML = '
' + (context.isModal ? '.' : ''); context.el = el; // The calender fires a blur event *every* time we redraw // this means we need to buffer the blur event to see if // it still has no focus after redrawing, and only then // do we return focus to the input. A possible other approach // would be to set context.redrawing = true on redraw and // set it to false in the blur event. var dp = el.querySelector('.dp'); on('blur', dp, buffer(10, function () { if (!dp.contains(document.activeElement)) { if (context.isModal) { input.focus(); } else if (input !== document.activeElement) { hideCalendar(context); } } })); forceDatesIntoMinMax(context); if (!context.isModal) { el.style.left = input.offsetLeft + 'px'; el.style.top = (input.offsetTop + input.offsetHeight) + 'px'; input.parentElement.insertBefore(el, input.nextSibling); } else { document.body.appendChild(el); } render(calHtml, context); // Prevent clicks on the wrapper's children from closing the modal on('mousedown', el, function (e) { if (e.target !== el && e.target.tagName !== 'A') { e.preventDefault(); } }); on('keydown', el, function (e) { // Prevent the window from scrolling around // when we are arrowing around the calendar. if (e.keyCode >= left && e.keyCode <= down) { e.preventDefault(); } if (el.querySelector('.dp-cal')) { calKeydown(e, el, context); } else if (el.querySelector('.dp-months')) { monthsKeydown(e, el, context); } else if (el.querySelector('.dp-years')) { yearsKeydown(e, el, context); } }); on('click', /dp-next/, el, function () { shiftMonth(context.currentDate, context.currentDate.getMonth() + 1); render(calHtml, context); }); on('click', /dp-prev/, el, function () { shiftMonth(context.currentDate, context.currentDate.getMonth() - 1); render(calHtml, context); }); on('click', /dp-day/, el, function (e) { context.onChange(new Date(parseInt(e.target.getAttribute('data-date')))); }); on('click', /dp-year/, el, function (e) { context.currentDate.setFullYear(parseInt(e.target.getAttribute('data-year'))); render(calHtml, context); }); on('click', /dp-month/, el, function(e) { context.currentDate.setMonth(parseInt(e.target.getAttribute('data-month'))); render(calHtml, context); }); on('click', /dp-cal-year/, el, function () { render(yearsHtml, context); }); on('click', /dp-cal-month/, el, function () { render(monthsHtml, context); }); on('click', /dp-today/, el, function () { context.onChange(now()); }); on('click', /dp-clear/, el, function () { context.onChange(null); }); on('click', /dp-close/, el, function () { input.focus(); // For dropdown calendars, we need to allow the focus // event to play out before hiding, or else the focus // event will re-show the calendar. !context.isModal && buffer(10, function () { hideCalendar(context); })(); }); } // Forces the context's dates to be within min/max function forceDatesIntoMinMax(context) { var input = context.input; var parsedValue = context.parse(input.value); context.currentDate = (inRange(context, parsedValue) ? parsedValue : new Date(context.min)); context.selectedDate = new Date(context.currentDate); input.value && (input.value = context.format(context.currentDate)); } function calKeydown(e, el, context) { var key = e.keyCode; if (key === left) { shiftDate(el, context, -1); } else if (key === right) { shiftDate(el, context, 1); } else if (key === up) { shiftDate(el, context, -7); } else if (key === down) { shiftDate(el, context, 7); } } function monthsKeydown(e, el, context) { var key = e.keyCode; if (key === left) { selectMonth(el, context, -1); } else if (key === right) { selectMonth(el, context, 1); } else if (key === up) { selectMonth(el, context, -3); } else if (key === down) { selectMonth(el, context, 3); } } function yearsKeydown(e, el, context) { var key = e.keyCode; if (key === left || key === up) { selectYear(e, el, context, -1); } else if (key === right || key === down) { selectYear(e, el, context, 1); } } function selectYear(e, el, context, amount) { e.preventDefault(); var newYear = context.currentDate.getFullYear() + amount; var validYear = Math.min(context.max.getFullYear(), Math.max(context.min.getFullYear(), newYear)); context.currentDate.setFullYear(validYear); render(yearsHtml, context); } function selectMonth(el, context, amount) { // This weird formula ensures the date stays within the current year var month = (12 + (context.currentDate.getMonth() + amount)) % 12; shiftMonth(context.currentDate, month); render(monthsHtml, context); } function shiftDate(el, context, amount) { var dt = context.currentDate; dt.setDate(dt.getDate() + amount); render(calHtml, context); } function on(evt, pattern, el, fn) { if (!fn) { fn = el; el = pattern; pattern = /./ } el.addEventListener(evt, function (e) { if (pattern.test(e.target.className)) { fn(e); } }, true); } // Renders HTML into context.el's container. // It keeps the focus on the input or calendar accordingly. function render(fn, context) { var inputFocused = context.input === document.activeElement; var html = fn(context); html && (context.el.firstChild.innerHTML = html); if (context.isModal || !inputFocused) { var current = context.el.querySelector('.dp-current'); return current && current.focus(); } } // Given the specified context, produces an HTML string // representing years. function yearsHtml(context) { var currentYear = context.currentDate.getFullYear(); if (smartRender(context, '[data-year="' + currentYear + '"]')) { return 0; } var selectedYear = context.selectedDate.getFullYear(); return ( '