A lightweight, accessible vanilla date time picker with calendar functionality, overwriting the user agent's default behavior.
Created
June 23, 2020 12:04
-
-
Save toomanyredirects/745d38d35cc7dc86230eecb41204a8dc to your computer and use it in GitHub Desktop.
Lightweight Vanilla Date Time Picker Calendar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<form class="presentation"> | |
<fieldset> | |
<label for="date-calendar">Modal Calendar</label> | |
<input type="text" id="date-calendar" data-datepicker="datetime" data-datepicker-mode="calendar" placeholder="Select date & time" required/> | |
</fieldset> | |
<fieldset> | |
<label for="datetime-local">Datetime-Local Input</label> | |
<input type="datetime-local" id="datetime-local" placeholder="Select date & time" required/> | |
</fieldset> | |
<fieldset> | |
<label for="date">Date Input</label> | |
<input type="date" id="date" placeholder="Select a date" required/> | |
<label for="time">Time Input</label> | |
<input type="time" id="time" placeholder="Select a time" required/> | |
</fieldset> | |
<fieldset> | |
<label for="month">Month Input</label> | |
<input type="month" id="month" placeholder="Select a month" required/> | |
</fieldset> | |
<fieldset> | |
<label for="dates-multi">Text Input with Multiple Dates</label> | |
<input type="text" id="dates-multi" data-datepicker="date" data-datepicker-mode="multiple" placeholder="Select multiple dates" required/> | |
</fieldset> | |
<fieldset> | |
<label for="years-range">Text Input with Year Range</label> | |
<input type="text" id="years-range" data-datepicker="year" data-datepicker-mode="range" placeholder="Select year range" required/> | |
</fieldset> | |
</form> | |
<script type="text/javascript"> | |
// Lightweight Vanilla Date Time Picker Calendar | |
document.addEventListener('DOMContentLoaded', function(e) { | |
// Presentation variables | |
var options = { | |
disablePast: false, | |
disableFuture: false, | |
disableWeekend: true, | |
weekStart: 0, | |
minStep: 15, | |
hrsStep: 1 | |
}; | |
var translation = { | |
minutes: 'Minuten', | |
hours: 'Stunden', | |
months: ['Januar','Febuar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'], | |
days: ['Montag','Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'], | |
format: 'yyyy-MM-dd hh:mm', dateformat: 'yyyy-MM-dd', timeformat: 'hh:mm', monthformat: 'yyyy-MM', longformat: 'd. MMMM yyyy', displayformat: 'D, d. MMMM yyyy', | |
calendar: 'Kalender', | |
previous: 'vorheriger', | |
next: 'nächster', | |
selected: 'ausgewählt', | |
select: 'auswählen', | |
today: 'heute', | |
set: 'wählen', | |
now: 'jetzt' | |
}; | |
// Options / translation de-CH: | |
Datepicker(options, translation); | |
// Default initialization | |
// Datepicker(); | |
// Open calendar | |
document.querySelector('form input').click(); | |
}, false); | |
</script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Lightweight Vanilla Date Time Picker Calendar | |
// --------------------------------------------- | |
// | |
// A lightweight, accessible vanilla date time picker with calendar functionality, overwriting | |
// the user agent's default date/week/month/time input behaviors. | |
// | |
// Compatibility: Javascript ES5, IE9+, Element.classList.add/remove/toggle omittet, | |
// no css flexbox used | |
// | |
// Autor: [email protected] | |
// Created: 2020.06.11, 17:00 | |
// Changed: 2020.06.21, 09:05 | |
function Datepicker (opt, lang, dts) { | |
var w = window, d = document, | |
options = { disablePast: false, disableFuture: false, disableWeekend: true, weekStart: 0, minStep: 15, hrsStep: 1 }, | |
i18n = { | |
minutes: 'minutes', | |
hours: 'hours', | |
months: ['January','Febuary','March','April','May','June','July','August','September','October','November','December'], | |
days: ['Monday','Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], | |
format: 'yyyy-MM-dd hh:mm', dateformat: 'yyyy-MM-dd', timeformat: 'hh:mm', monthformat: 'yyyy-MM', longformat: 'MMMM d, yyyy', displayformat: 'D, MMMM d yyyy', | |
calendar: 'calendar', | |
previous: 'previous', | |
next: 'next', | |
selected: 'selected', | |
select: 'select', | |
today: 'today', | |
set: 'set', | |
now: 'now' | |
}, | |
keys = { esc: 27, space: 32, pgup: 33, pgdown: 34, end: 35, home: 36, left: 37, up: 38, right: 39, down: 40 }, | |
icons = { | |
left: '<path d="M2.4,4l2.8-2.8L4.5,0.5l-4,4l4,4l0.7-0.7L2.4,5h6.1V4H2.4z"/>', | |
right: '<path d="M0.5,4v1h6.1L3.8,7.8l0.7,0.7l4-4l-4-4L3.8,1.2L6.6,4H0.5z"/>', | |
calendar: '<path d=""/>', | |
clock: '<path d=""/>' | |
}; | |
// Merge options and language objects | |
if (opt) mergeObj(options, opt); | |
if (opt) mergeObj(i18n, lang); | |
// Layout template | |
var tpl = [ | |
'<div class="datepicker-header"><h2 aria-live="polite">'+ i18n.calendar +'</h2><time aria-live="polite"></time></div>', | |
'<div class="datepicker-navigation" role="navigation">', | |
' <button type="button" class="prev" aria-label="'+ i18n.previous +'"><svg viewBox="0 0 9 9" role="illustration">'+ icons.left +'</svg></button>', | |
' <h3 id="datepicker-label" aria-live="polite" tabindex="0">Month Year</h3>', | |
' <button type="button" class="next" aria-label="'+ i18n.next +'"><svg viewBox="0 0 9 9" role="illustration">'+ icons.right +'</svg></button>', | |
'</div>', | |
'<div class="datepicker-body">', | |
' <form class="time">', | |
' <div class="hours"><label>'+ i18n.hours +'</label>', | |
' <button type="button" role="spinbutton">+</button><input type="number" step="'+ options.hrsStep +'" min="0" max="23"/><button type="button" role="spinbutton">−</button>', | |
' </div>', | |
' <div class="minutes"><label>'+ i18n.minutes +'</label>', | |
' <button type="button" role="spinbutton">+</button><input type="number" step="'+ options.minStep +'" min="0" max="59"/><button type="button" role="spinbutton">−</button>', | |
' </div>', | |
' </form>', | |
' <div class="month" role="grid" aria-labelledby="datepicker-label">', | |
' <div class="week" scope="col"></div>', | |
' <div class="days" scope="col"></div>', | |
' </div>', | |
' <div class="year"></div>', | |
' <div class="decade"></div>', | |
'</div>', | |
'<div class="datepicker-footer"><button type="button">'+ i18n.select +'</button></div>', | |
'<i class="indicator"></i>' | |
].join(''); | |
// Trigger selectors | |
var triggers = d.querySelectorAll('[type*="date"], [type="month"], [type="time"], [type="week"], [data-datepicker]'); | |
[].slice.call(triggers).forEach(function(trigger){ init(trigger); }); | |
// Datepicker | |
function init (trigger) { | |
var picker, indicator, header, footer, navigation, title, | |
current, time, minutes, hours, days, week, month, year, decade, | |
label, pButton, nButton, aButton; | |
var type = trigger.getAttribute('type') !== 'text' ? trigger.getAttribute('type') : trigger.getAttribute('data-datepicker') || 'date'; | |
type = type.split('-')[0].toLowerCase(); | |
var mode = trigger.getAttribute('data-datepicker-mode') || 'single', | |
view = trigger.getAttribute('data-datepicker-view') || type, | |
offset = trigger.getAttribute('data-datepicker-offset') || 5, | |
date = new Date(), today = new Date(), | |
selected = trigger.value ? [formatDate(new Date(trigger.value), i18n.dateformat)] : []; | |
trigger.setAttribute('aria-haspopup', true); | |
trigger.setAttribute('aria-expanded', false); | |
// Trigger event listener | |
trigger.onfocus = prevent; | |
trigger.oninput = prevent; | |
trigger.addEventListener('click', toggle, false); | |
createPicker(); | |
// Picker methods | |
function pick (e) { | |
if(e) e.preventDefault(); | |
var el = this, | |
dt = new Date(el.firstChild.getAttribute('datetime')), | |
val, string = formatDate(dt, i18n.dateformat), | |
isSelected = el.hasAttribute('aria-selected'); | |
switch(type) { | |
case 'datetime': val = formatDate(dt, i18n.format).replace(' ','T'); break; | |
case 'time': val = formatDate(dt, i18n.timeformat); break; | |
case 'month': val = formatDate(dt, i18n.monthformat); break; | |
default: val = string; | |
} | |
if (view.match(/(decade|year|month)/g)){ | |
date.setFullYear(dt.getFullYear()); | |
if (view === 'year') date.setMonth(dt.getMonth()); | |
if (view === 'month') date.setDate(dt.getDate()); | |
if (view === 'year' && type !== 'month') monthView(); | |
else if (view === 'decade' && type !== 'year') yearView(); | |
else if (view === 'month' && type === 'datetime') timeView(); | |
else isSelected ? deselect(el, string, val) : select(el, string, val); | |
} | |
else isSelected ? deselect(el, string, val) : select(el, string, val); | |
} | |
function toggle (e) { | |
if(e) e.preventDefault(); | |
var v = picker.className.match(/\bshow\b/g), | |
ops = d.querySelectorAll('.datepicker.show'), | |
fcs = d.querySelectorAll('input.focus'); | |
[].slice.call(ops).forEach(function(o){ o.className = o.className.replace(/\bshow\b/g,'').trim(); }); | |
[].slice.call(fcs).forEach(function(f){ f.className = f.className.replace(/\bfocus\b/g,'').trim(); }); | |
picker.className = v ? picker.className.replace(/\bshow\b/g,'').trim() : picker.className +' show'; | |
trigger.setAttribute('aria-expanded', v ? false : true); | |
trigger.className = v ? trigger.className.replace(/\bfocus\b/g,'').trim() : trigger.className +' focus'; | |
place(picker, trigger); | |
if(mode.match(/(calendar|modal)/gi)) backdrop(v); | |
trigger.blur() | |
} | |
function select (el, string, val) { | |
if (!mode.match(/(multiple|range)/gi)) { | |
selected = [string]; | |
[].slice.call(picker.querySelectorAll('[aria-selected]')).forEach(function(l){ | |
l.removeAttribute('aria-selected'); | |
}); | |
trigger.value = val; | |
toggle(); | |
} | |
else if (mode.match(/multiple/gi)) trigger.value = selected.toString(); | |
else selected = mergeArr(selected, [string]); | |
el.title = ucFirst(i18n.selected); | |
el.setAttribute('aria-selected', true); | |
} | |
function deselect (el, string, val) { | |
if(string) selected = selected.filter(function(v, i, selected){ return v === string; }); | |
if(!el || !mode.match(/(multiple|range)/gi)) selected.forEach(function(ref) { | |
el = picker.querySelector('[href="#'+ ref +'"]'); | |
if(el) { el.removeAttribute('aria-selected'); el.removeAttribute('title'); } | |
trigger.value = ''; | |
}); | |
else { el.removeAttribute('aria-selected'); el.removeAttribute('title'); } | |
} | |
function disable (el) { | |
el.setAttribute('draggable', false); | |
el.setAttribute('disabled', ''); | |
el.setAttribute('tabindex', '-1'); | |
} | |
function enable (el) { | |
el.setAttribute('draggable', true); | |
el.removeAttribute('disabled', ''); | |
el.onclick = pick; | |
el.ondrag = drag; | |
trigger.ondragover = over; | |
trigger.ondrop = drop; | |
function drag(e) { e.dataTransfer.dropEffect = 'link'; } | |
function over(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } | |
function drop(e) { e.preventDefault(); el.click(); } | |
} | |
function createPicker () { | |
picker = d.createElement('div'); | |
picker.setAttribute('role', 'modal'); | |
picker.setAttribute('aria-modal', true); | |
picker.setAttribute('aria-labelledby', 'datepicker-label'); | |
picker.setAttribute('tabindex', 0); | |
picker.className = 'datepicker'; | |
picker.innerHTML = tpl; | |
// Assign ui controls | |
header = picker.querySelector('.datepicker-header'); | |
footer = picker.querySelector('.datepicker-footer'); | |
navigation = picker.querySelector('.datepicker-navigation'); | |
title = header.querySelector('time'); | |
time = picker.querySelector('form.time'); | |
minutes = time.querySelector('.minutes'); | |
hours = time.querySelector('.hours'); | |
month = picker.querySelector('.month'); | |
week = month.querySelector('.week'); | |
days = month.querySelector('.days'); | |
year = picker.querySelector('.year'); | |
decade = picker.querySelector('.decade'); | |
label = navigation.querySelector('h3'), | |
nButton = navigation.querySelector('.next'), | |
pButton = navigation.querySelector('.prev'), | |
aButton = footer.querySelector('button'), | |
// Inject & call view | |
trigger.parentElement.appendChild(picker); | |
picker.addEventListener('keyup', keyup, false); | |
switch(type) { | |
case 'time': timeView(); break; | |
case 'month': yearView(); break; | |
case 'year': decadeView(); break; | |
default: monthView(); | |
} | |
} | |
// Picker backdrop | |
function backdrop (hide){ | |
picker.className += ' modal'; | |
var drop = d.querySelector('.datepicker-backdrop'); | |
if(!drop){ drop = d.createElement('div'); picker.insertAdjacentElement('afterend', drop); } | |
drop.className = 'datepicker-backdrop'; | |
if(!hide) drop.onclick = function(e) { toggle(e); }; | |
hide ? drop.setAttribute('hidden','') : drop.removeAttribute('hidden'); | |
} | |
// Picker placing | |
function place(el, target) { | |
if (!el.className.match(/\bshow\b/g) || mode === 'calendar') return false; | |
var x, y, eP = pos(el), tP = pos(target), iP = indicator ? pos(indicator) : null, | |
sY = w.pageYOffset, sX = w.pageXOffset, vw = d.documentElement.clientWidth, vh = w.innerHeight; | |
x = tP.left + tP.width - eP.width - sX; | |
y = tP.top - eP.height - offset - sY; | |
el.className = el.className.replace(/\b(left|right|top|bottom)\b/g, '').trim(); | |
if (x <= 0) {x = offset; el.className = el.className.replace(/\bleft\b/g,'').trim() +' right';} | |
if (y <= 0) {y = tP.top + tP.height + offset; el.className += ' bottom';} | |
if (x >= vw) el.className = el.className.replace(/\bright\b/g,'').trim() +' left'; | |
if (y >= vh) el.className = el.className.replace(/\bbottom\b/g,'').trim(); | |
el.setAttribute('style','left:'+ x +'px;top:'+ y +'px;'); | |
// Overwite previous listeners | |
w.onscroll = function(){ place(el, target); }; | |
w.onresize = function(){ place(el, target); }; | |
d.onorientationchange = function(){ place(el, target); }; | |
//Position method | |
function pos(el) { | |
var b = el.getBoundingClientRect(); | |
return { top: b.top + w.pageYOffset, left: b.left + w.pageXOffset, width: b.width, height: b.height }; | |
} | |
} | |
// Picker methods | |
function setClock () { | |
var mm = minutes.querySelector('input'), | |
hh = hours.querySelector('input'); | |
mm.value = zero(date.getMinutes()); | |
hh.value = zero(date.getHours()); | |
spinner(mm); | |
spinner(hh); | |
time.addEventListener('change', set, false); | |
function set (e) { | |
if(type === 'time') trigger.value = hh.value +'-'+ mm.value +'-00'; | |
trigger.value = hh.value +'-'+ mm.value; | |
} | |
function check (e) { | |
var val = parseInt(this.value, 10), | |
coe = parseInt(this.step, 10), | |
min = parseInt(this.min, 10), | |
max = parseInt(this.max, 10); | |
if (val < min) this.value = Math.ceil(max/coe)*coe; | |
else if (val > max) this.value = Math.ceil(min/coe)*coe; | |
else if (isNaN(val)) this.value = Math.ceil(min/coe)*coe; | |
this.value = zero(this.value); | |
} | |
function spinner (el) { | |
var step = parseInt(el.step, 10) || 1, | |
plus = el.parentNode.querySelector('button:first-of-type'), | |
minus = el.parentNode.querySelector('button:last-of-type'); | |
plus.onclick = function(e) { change(el, step); }; | |
minus.onclick = function(e) { change(el, -step); }; | |
el.oninput = check; | |
el.onchange = check; | |
} | |
function change (el, num) { | |
el.value = zero(parseInt(el.value, 10) + num); | |
"createEvent" in d ? el.dispatchEvent(new Event('change')) : el.fireEvent('onchange'); | |
} | |
function zero (int) { | |
return ('00'+ int).slice(-2); | |
} | |
} | |
function createDay (num, day, y) { | |
var el = d.createElement('a'), | |
ti = d.createElement('time'), | |
string = formatDate(date, i18n.dateformat), | |
match = date.toString() === today.toString(); | |
ti.innerHTML = num; | |
el.href = '#'+ string; | |
ti.setAttribute('datetime', formatDate(date, i18n.format)); | |
if (num === 1) { | |
if (day === 0) el.style.marginLeft = (6 * 14.28) + '%'; | |
else el.style.marginLeft = ((day - 1) * 14.28) + '%'; | |
} | |
if (options.disablePast && date.getTime() <= today.getTime() - 1) disable(el); | |
else if (options.disableWeekend && (day == 0 || day == 6)) disable(el); | |
else enable(el); | |
if (match) { | |
el.className += ' today'; | |
el.title = ucFirst(i18n.today); | |
ti.setAttribute('datetime', formatDate(date, i18n.format)); | |
} | |
if (selected.indexOf(string) > -1) el.setAttribute('aria-selected', true); | |
el.appendChild(ti); | |
days.appendChild(el); | |
} | |
function createWeekdays () { | |
week.innerHTML = ''; | |
i18n.days.forEach(function(name) { | |
var el = d.createElement('abbr'); | |
el.setAttribute('scope', 'col'); | |
el.setAttribute('title', name); | |
el.setAttribute('data-content', name.substring(0, 1)); | |
el.setAttribute('data-twochar', name.substring(0, 2)); | |
el.setAttribute('data-threechar', name.substring(0, 3)); | |
el.innerHTML = name; | |
week.appendChild(el); | |
}); | |
} | |
function createMonths () { | |
year.innerHTML = ''; | |
i18n.months.forEach(function(name, i) { | |
var a = d.createElement('a'), | |
ti = d.createElement('time'), | |
cur = new Date(date.getFullYear(), i+1, 0), | |
match = cur.getMonth() === today.getMonth() && cur.getFullYear() === today.getFullYear(); | |
ti.innerHTML = name; | |
ti.setAttribute('datetime', formatDate(cur, i18n.monthformat)); | |
a.href = '#'+ formatDate(cur, i18n.monthformat); | |
a.setAttribute('title', name +' '+ cur.getFullYear()); | |
a.setAttribute('data-content', name.substring(0, 3)); | |
if (match) a.className += ' current'; | |
a.addEventListener('click', pick, false); | |
a.appendChild(ti); | |
year.appendChild(a); | |
}); | |
} | |
function createYears (num) { | |
var years = [], span = yearSpan(date.getFullYear(), num); | |
for (var i = span[0]; i <= span[1]; i++) years.push(i); | |
decade.innerHTML = ''; | |
years.forEach(function(name, i) { | |
var a = d.createElement('a'), | |
ti = d.createElement('time'), | |
cur = new Date(name, date.getMonth()+1, 0), | |
match = name === today.getFullYear(); | |
ti.innerHTML = name; | |
ti.setAttribute('datetime', formatDate(cur, i18n.monthformat)); | |
a.href = '#'+ formatDate(cur, i18n.monthformat); | |
if (match) a.className += ' current'; | |
a.addEventListener('click', pick, false); | |
a.appendChild(ti); | |
decade.appendChild(a); | |
}); | |
} | |
// Views | |
// View setter | |
function setView () { | |
var el = picker.querySelector('.'+ view), | |
active = el.querySelector('a:not([disabled]):first-of-type, .today:not([disabled]), [aria-selected]:not([disabled])'), | |
p = date.getMonth() - 1, pDate = new Date(date.getFullYear(), p, 0), | |
n = date.getMonth() + 1, nDate = new Date(date.getFullYear(), n, 0); | |
label.innerHTML = i18n.months[date.getMonth()] +' '+ date.getFullYear(); | |
navigation.removeAttribute('hidden'); | |
header.setAttribute('hidden',''); | |
aButton.setAttribute('hidden',''); | |
nButton.removeAttribute('hidden'); | |
if(mode === 'calendar'){ | |
var match = date.toString() === today.toString(); | |
header.removeAttribute('hidden',''); | |
title.innerHTML = formatDate(date, i18n.displayformat); | |
title.setAttribute('datetime', formatDate(date, i18n.format)); | |
if (match) header.querySelector('h2').innerHTML = i18n.today; | |
} | |
if(view === 'month' && type === 'date'){ | |
aButton.removeAttribute('hidden',''); | |
} | |
if(view === 'year') { | |
p = date.getFullYear() - 1; pDate = new Date(p, 0, 0); | |
n = date.getFullYear() + 1; nDate = new Date(n, 0, 0); | |
label.innerHTML = date.getFullYear(); | |
} | |
if(view === 'decade') { | |
p = yearSpan(date.getFullYear(), 10)[0] - 1; pDate = new Date(p, 0, 0); | |
n = yearSpan(date.getFullYear(), 10)[1] + 1; nDate = new Date(n, 0, 0); | |
label.innerHTML = (p+1) +' - '+ (n-1); | |
} | |
if(view === 'time') { | |
if(type === 'time') navigation.setAttribute('hidden',''); | |
nButton.setAttribute('hidden',''); | |
aButton.removeAttribute('hidden',''); | |
label.innerHTML = formatDate(date, i18n.longformat); | |
} | |
// Reset event listeners | |
aButton.onclick = set; | |
pButton.onclick = previous; | |
nButton.onclick = next; | |
label.onclick = index; | |
// Define future / past dates | |
var isPast = nDate.getTime() < today.getTime(), | |
isFuture = pDate.getTime() > today.getTime(); | |
// Set and display view | |
current = { name: view, el: el, items: el.querySelectorAll('a'), prev: pButton, next: nButton }; | |
display(); | |
// Methods | |
function display () { | |
var vs = picker.querySelectorAll('.show'); | |
[].slice.call(vs).forEach(function(v){ v.className = v.className.replace(/\bshow\b/g,'').trim(); }); | |
el.className = (el.className + ' show').trim(); | |
if(view !== 'time') active ? active.focus() : current.items[0].focus(); | |
} | |
function previous (e) { | |
if(options.disablePast && isPast) return false; | |
if(view === 'time') monthView(); | |
else if(view === 'year') yearView(p); | |
else if(view === 'decade') decadeView(p); | |
else monthView(p); | |
} | |
function next (e) { | |
if(options.disableFuture && isFuture) return false; | |
if(view === 'year') yearView(n); | |
else if(view === 'decade') decadeView(n); | |
else monthView(n); | |
} | |
function index (e) { | |
if(view === 'decade') return false; | |
else if(view === 'year') decadeView(); | |
else if(view === 'time') monthView(); | |
else yearView(); | |
} | |
function set (e) { | |
if(type.match(/(time|date)/gi)){ | |
trigger.value = type === 'time' ? formatDate(date, i18n.timeformat) : formatDate(date, i18n.dateformat); | |
toggle(); | |
} | |
} | |
} | |
// Views | |
function timeView (hrs, min) { | |
view = 'time'; | |
if(hrs) date.setHours(hrs); | |
if(min) date.setMinutes(min); | |
setClock(); | |
setView(); | |
} | |
function monthView (num) { | |
view = 'month'; | |
if(num) date.setMonth(num); | |
date.setDate(1); | |
var cur = date.getMonth(); | |
createWeekdays(); | |
days.innerHTML = ''; | |
while (date.getMonth() === cur) { | |
createDay(date.getDate(), date.getDay(), date.getFullYear() ); | |
date.setDate(date.getDate() + 1); | |
} | |
// while loop trips over and day is at 30/31, bring it back | |
date.setDate(1); | |
date.setMonth(date.getMonth() - 1); | |
setView(); | |
} | |
function yearView (num) { | |
view = 'year'; | |
if(num) date.setFullYear(num); | |
createMonths(); | |
setView(); | |
} | |
function decadeView (num) { | |
view = 'decade'; | |
if(num) date.setFullYear(num); | |
createYears(10); | |
setView(); | |
} | |
// Keyboard events | |
function keyup (e) { | |
var fl = false, s = view === 'month' ? 7 : 3, | |
el = current.el.querySelector(':not([disabled]):focus, [selected]:not([disabled])') || current.items[0], | |
index = [].indexOf.call(current.items, el); | |
switch (e.keyCode) { | |
case keys.esc: toggle(e); break; | |
case keys.space: if(el) el.click(); break; | |
case keys.right: el && el.nextSibling ? el.nextSibling.focus() : toNext; fl = true; break; | |
case keys.left: el && el.previousSibling ? el.previousSibling.focus() : toPrevious; fl = true; break; | |
case keys.down: typeof current.items[index+s] !== 'undefined' ? current.items[index+s].focus() : toNext; fl = true; break; | |
case keys.up: typeof current.items[index-s] !== 'undefined' ? current.items[index-s].focus() : toPrevious; fl = true; break; | |
case keys.pgup: toPrevious(); fl = true; break; | |
case keys.pgdown: toNext(); fl = true; break; | |
case keys.home: current.items[0].focus(); fl = true; break; | |
case keys.end: current.items[current.items.length - 1].focus(); fl = true; break; | |
} | |
if (fl) prevent; | |
// Methods | |
function toPrevious () { current.prev.click(); }; | |
function toNext () { current.next.click(); }; | |
} | |
} | |
// Helper | |
function prevent (e) { | |
e.stopPropagation(); e.preventDefault(); | |
} | |
function mergeObj (o1, o2) { | |
for (var key in o2) o1[key] = o2[key]; return o1; | |
} | |
function mergeArr (a1, a2) { | |
return a1.concat(a2.filter(function (m) { return a1.indexOf(m) === -1; })); | |
} | |
function ucFirst (string) { | |
return string.charAt(0).toUpperCase() + string.slice(1); | |
} | |
function yearSpan (number, interval) { | |
var s = Math.floor(number/interval)*interval; | |
return [s, s+interval-1]; | |
} | |
function formatDate (date, format) { | |
var z = { M: date.getMonth() + 1, d: date.getDate(), h: date.getHours(), m: date.getMinutes(), s: date.getSeconds() }; | |
format = format.replace(/\b(M{1,2}|d{1,2}|h{1,2}|m{1,2}|s{1,2})\b/g, function(v) { | |
return ((v.length > 1 ? '0' : '') + z[v.slice(-1)]).slice(-2); | |
}); | |
format = format.replace(/(y+)/g, function(v) { | |
return date.getFullYear().toString().slice(-v.length); | |
}); | |
format = format.replace(/(D|ddd+)/g, i18n.days[date.getDay() - 1]); | |
format = format.replace(/(MMM+)/g, i18n.months[date.getMonth()]); | |
return format; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Pen | |
// Font stacks | |
@import url(https://fonts.googleapis.com/css?family=Montserrat:200,400,700); | |
$font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif !default; | |
*, *::before, *::after { | |
box-sizing: border-box; | |
} | |
html, body { | |
height: 100%; | |
width: 100%; | |
} | |
body { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin: 0; | |
font-family: Montserrat, $font-stack; | |
color: #222; | |
} | |
.presentation { | |
&, fieldset { | |
border: 0; min-width: 15rem; | |
} | |
label { | |
line-height: 1; | |
padding: 0; | |
margin: 0 0 .5em 0; | |
white-space: nowrap; | |
float: left; | |
&:not(only-child) { | |
width: 60%; | |
} | |
& ~ label { | |
margin-top: -1.5em; | |
width: 35%; | |
float: right; | |
} | |
} | |
input { | |
width: 100%; | |
float: left; | |
margin: 0; | |
&[type="date"]:not(only-child) { | |
width: 60%; | |
} | |
&[type="time"]:not(only-child) { | |
width: 35%; | |
float: right; | |
} | |
} | |
} | |
// Lightweight Vanilla Date Time Picker | |
$datepicker-font-color: #222 !default; | |
$datepicker-bg: #fff !default; | |
$datepicker-dark-bg: #999 !default; | |
$datepicker-spacing: 1rem !default; | |
$datepicker-max-size: 29rem !default; | |
$datepicker-min-size: 20rem !default; | |
$datepicker-border-radius: .3rem !default; | |
$datepicker-border-color: #e7e9ed !default; | |
$datepicker-box-shaddow: 0 4px 22px 0 rgba(0, 0, 0, 0.05) !default; | |
$datepicker-header-bg: #109899 !default; | |
$datepicker-header-color: #fff !default; | |
$datepicker-today-bg: #109899 !default; | |
$datepicker-today-color: #fff !default; | |
$datepicker-today-indicator-bg: orange !default; | |
$datepicker-focus-color: #333 !default; | |
$datepicker-selected-bg: #E77 !default; | |
$datepicker-selected-color: #FFF !default; | |
$datepicker-zindex: 1020 !default; | |
$datepicker-breakpoint-sm: 360px !default; | |
$datepicker-breakpoint-md: 768px !default; | |
$datepicker-breakpoint-lg: 1098px !default; | |
$datepicker-breakpoint-height: 640px !default; | |
@function str-replace($string, $search, $replace: '') { | |
$index: str-index($string, $search); | |
@if $index { @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); } | |
@return $string; | |
} | |
:root { | |
--ui-datepicker-bg: #{$datepicker-bg}; | |
--ui-datepicker-border-radius: #{$datepicker-border-radius}; | |
--ui-datepicker-border-color: #{$datepicker-border-color}; | |
--ui-datepicker-today-bg: #{$datepicker-today-bg}; | |
--ui-datepicker-today-color: #{$datepicker-today-color}; | |
--ui-datepicker-selected-bg: #{$datepicker-selected-bg}; | |
--ui-datepicker-selected-color: #{$datepicker-selected-color}; | |
--ui-datepicker-zindex: #{$datepicker-zindex}; | |
--ui-datepicker-breakpoint-sm: #{$datepicker-breakpoint-sm}; | |
--ui-datepicker-breakpoint-md: #{$datepicker-breakpoint-md}; | |
--ui-datepicker-breakpoint-lg: #{$datepicker-breakpoint-lg}; | |
--ui-datepicker-breakpoint-height: #{$datepicker-breakpoint-height}; | |
} | |
// Inputs | |
input { | |
appearance: none; | |
display: inline-block; | |
padding: .25em .5em; | |
font: inherit; | |
line-height: 1.5; | |
border: .075em solid currentColor; | |
border-radius: $datepicker-border-radius; | |
outline: 0; | |
transition: color .2s ease, box-shadow .2s ease, background .2s ease; | |
&::placeholder { | |
opacity: .5; | |
} | |
&:focus, &.focus { | |
color: $datepicker-today-bg; | |
box-shadow: 0 0 0 .2rem rgba($datepicker-today-bg, 0.25); | |
&::placeholder { | |
color: $datepicker-today-bg; | |
opacity: 1; | |
} | |
} | |
&::selection { | |
color: $datepicker-today-color; | |
background: $datepicker-today-bg; | |
} | |
&[type="time"], | |
&[type*="date"], | |
&[type="week"], | |
&[type="month"], | |
&[type="year"], | |
&[data-datepicker]{ | |
background-repeat: no-repeat; | |
background-size: 1.25em 1.25em; | |
background-position: calc(100% - .55em) center; | |
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 9'%3e%3cpath d='M1.8.5v.8h-.7c-.2 0-.4.2-.4.4v6.5c0 .2.2.4.4.4H8c.2 0 .4-.2.4-.4V1.6c0-.2-.2-.4-.4-.4h-.8V.5H6v.8H3V.5H1.8zm-.3 3h6.1v4.2H1.5V3.5zm.3.4v1.5h1.5V3.9H1.8zm1.9 0v1.5h1.5V3.9H3.7zm1.9 0v1.5h1.5V3.9H5.6zM1.8 5.8v1.5h1.5V5.8H1.8zm1.9 0v1.5h1.5V5.8H3.7z' fill='#{str-replace(''+$datepicker-font-color, '#', '%23')}'/%3e%3c/svg%3e"); | |
cursor: default; | |
user-select: none!important; | |
&:focus, &.focus { | |
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 9'%3e%3cpath d='M1.8.5v.8h-.7c-.2 0-.4.2-.4.4v6.5c0 .2.2.4.4.4H8c.2 0 .4-.2.4-.4V1.6c0-.2-.2-.4-.4-.4h-.8V.5H6v.8H3V.5H1.8zm-.3 3h6.1v4.2H1.5V3.5zm.3.4v1.5h1.5V3.9H1.8zm1.9 0v1.5h1.5V3.9H3.7zm1.9 0v1.5h1.5V3.9H5.6zM1.8 5.8v1.5h1.5V5.8H1.8zm1.9 0v1.5h1.5V5.8H3.7z' fill='#{str-replace(''+$datepicker-today-bg, '#', '%23')}'/%3e%3c/svg%3e"); | |
} | |
&::-webkit-spin-button, | |
&::-webkit-outer-spin-button, | |
&::-webkit-inner-spin-button { | |
display: none; | |
} | |
&::-webkit-clear-button { | |
display: none; | |
} | |
&::-ms-clear { | |
display: none; | |
} | |
&::-webkit-calendar-picker-indicator { | |
right: 1.25em; | |
width: 1em; | |
opacity: 0; | |
pointer-events: none; | |
margin:0; | |
} | |
} | |
&[type="time"] { | |
/* autoprefixer: ignore next */ | |
-moz-appearance: textfield; | |
} | |
&[type="time"], | |
&[data-datepicker-mode="time"]{ | |
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 9'%3e%3cpath d='M4.5.5c-2.2 0-4 1.8-4 4s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4zm1.8 5.2l-.4.1-.2-.1-1.4-1-.1-.1v-.1-.1-.1-.1L5 1.3c0-.2.2-.3.4-.2.2.1.3.3.3.5L5 4.3l1.2.8c.1.2.2.4.1.6z' fill='#{str-replace(''+$datepicker-font-color, '#', '%23')}'/%3e%3c/svg%3e"); | |
&:focus, &.focus { | |
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 9'%3e%3cpath d='M4.5.5c-2.2 0-4 1.8-4 4s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4zm1.8 5.2l-.4.1-.2-.1-1.4-1-.1-.1v-.1-.1-.1-.1L5 1.3c0-.2.2-.3.4-.2.2.1.3.3.3.5L5 4.3l1.2.8c.1.2.2.4.1.6z' fill='#{str-replace(''+$datepicker-today-bg, '#', '%23')}'/%3e%3c/svg%3e"); | |
} | |
} | |
} | |
// Picker | |
.datepicker { | |
position: absolute; | |
display: inline-block; | |
margin: 0; | |
overflow: visible; | |
width: $datepicker-min-size; | |
font-size: 1rem; | |
color: $datepicker-font-color; | |
background-color: $datepicker-bg; | |
border-radius: $datepicker-border-radius; | |
border: 1px solid $datepicker-border-color; | |
box-shadow: $datepicker-box-shaddow; | |
outline: none; | |
z-index: $datepicker-zindex; | |
opacity: 0; | |
will-change: opacity, transform; | |
transition: opacity .2s linear, transform .2s ease; | |
&, * { user-select: none; } | |
[hidden] { display: none!important; visibility: hidden; } | |
&:not(.show) { display: none; } | |
&.show { | |
display: block; | |
opacity: 1; | |
} | |
&.slide-in { | |
transition: opacity .2s ease, transform .2s linear; | |
&:not(.show) { opacity: 0; transform: translate(0, 0); } | |
} | |
&.modal { | |
position: absolute; | |
top: 10%; left: 50%; | |
transform: translateX(-50%); | |
overflow: visible; | |
border: 0; | |
width: $datepicker-min-size; | |
@media (min-width: $datepicker-breakpoint-md ) { | |
width: 2*$datepicker-min-size; | |
& > div { | |
display:block; | |
width: 50.5%; | |
float: right; | |
} | |
.datepicker-header { | |
position: absolute; | |
bottom: 0; top:0; left: 0; | |
height: 100%; | |
border-radius: $datepicker-border-radius 0 0 $datepicker-border-radius; | |
time { | |
position: absolute; | |
font-size: 1.2rem; | |
bottom: $datepicker-spacing; | |
} | |
} | |
.datepicker-navigation { | |
clear: left; | |
} | |
} | |
} | |
&-backdrop { | |
position: fixed; | |
background: rgba(0,0,0,.25); | |
left: 0; right: 0; top:0; bottom:0; | |
z-index: $datepicker-zindex - 1; | |
} | |
.indicator { | |
position: absolute; | |
top: auto; | |
right: auto; | |
bottom: -$datepicker-spacing/2; | |
left: calc(50% - #{$datepicker-spacing}/2); | |
display: inline-block; | |
color: $datepicker-bg; | |
border: $datepicker-spacing/2 solid transparent; | |
border-bottom: 0; | |
border-top: $datepicker-spacing/2 solid currentColor; | |
width: 0; | |
height: 0; | |
} | |
&.bottom .indicator { | |
top: -$datepicker-spacing/2; | |
bottom: auto; | |
border-top: 0; | |
border-bottom: $datepicker-spacing/2 solid currentColor; | |
} | |
&.left .indicator { | |
top: calc(50% - #{$datepicker-spacing}/2); | |
right: -$datepicker-spacing/2; | |
left: auto; | |
bottom: auto; | |
border-top: 0; | |
border-right: $datepicker-spacing/2 solid currentColor; | |
} | |
&.right .indicator { | |
top: calc(50% - #{$datepicker-spacing}/2); | |
right: auto; | |
left: -$datepicker-spacing/2; | |
bottom: auto; | |
border-top: 0; | |
border-left: $datepicker-spacing/2 solid currentColor; | |
} | |
button { | |
appearance: none; | |
min-width: 1rem; | |
overflow: visible; | |
padding: 0; | |
margin: 0; | |
border: 0; | |
background: transparent; | |
color: inherit; | |
font: inherit; | |
line-height: normal; | |
text-align: center; | |
cursor: pointer; | |
outline: none; | |
border-radius: $datepicker-border-radius; | |
&:first-letter { | |
text-transform: uppercase; | |
} | |
&:hover { | |
color: $datepicker-today-bg; | |
} | |
&:active { | |
background-color: $datepicker-today-bg; | |
} | |
&:focus { | |
color: white; | |
background-color: $datepicker-today-bg; | |
box-shadow: 0 0 0 2px rgba($datepicker-today-bg, 0.1); | |
} | |
svg { | |
fill: currentColor; | |
} | |
} | |
&-header, &-navigation, &-body, &-footer { | |
display: inline-block; | |
width: 100%; | |
margin: 0; | |
padding: $datepicker-spacing; | |
} | |
&-header { | |
background: $datepicker-header-bg; | |
border-radius: $datepicker-border-radius $datepicker-border-radius 0 0 ; | |
h2, time { | |
color: $datepicker-header-color; | |
&:first-letter { | |
text-transform: uppercase; | |
} | |
} | |
h2 { | |
font-weight: 400; | |
width: 100%; | |
font-variant-numeric: ordinal; | |
margin: 0 0 2rem 0; | |
} | |
time { | |
color: $datepicker-header-color; | |
font-weight: 200; | |
font-size: 1.15rem; | |
} | |
&:not([hidden]) ~ .indicator { | |
display: none; | |
} | |
} | |
&-navigation { | |
position: relative; | |
button { | |
position: absolute; | |
width: 1.5rem; height: 1.5rem; | |
padding: .25rem; | |
line-height:1; | |
top: $datepicker-spacing*1.05; | |
&.prev { left: $datepicker-spacing; } | |
&.next { right: $datepicker-spacing;} | |
svg { | |
width:1rem; height: 1rem; | |
} | |
} | |
h3 { | |
margin: 0 auto; | |
padding: 0; | |
line-height: 1.5; | |
text-align: center; | |
border-radius: $datepicker-border-radius; | |
font-size: 1.1em; | |
width: calc(100% - 4em); | |
cursor: pointer; | |
font-variant-numeric: tabular-nums; | |
outline: 0; | |
&:hover { | |
color: $datepicker-today-bg; | |
} | |
&:focus { | |
color: $datepicker-today-color; | |
background-color: $datepicker-today-bg; | |
box-shadow: 0 0 0 2px rgba($datepicker-today-bg, 0.1); | |
} | |
} | |
} | |
&-body { | |
padding-bottom: 0; | |
} | |
&-footer { | |
text-align: center; | |
} | |
.time, .month, .year, .decade { | |
display: none; | |
&.show { display: block; } | |
} | |
.week, .days { | |
display: inline-block; | |
margin: 0; padding:0; | |
font-size: 0.9em; | |
abbr, a { | |
display: inline-block; | |
overflow: hidden; | |
width: 14.28%; | |
max-width: 14.28%; | |
margin: 0; | |
text-decoration: none; | |
text-align: center; | |
} | |
} | |
.week { | |
padding-bottom: 0; | |
abbr { | |
position: relative; | |
padding: 0; | |
overflow: hidden; | |
color: transparent; | |
&:first-letter { | |
text-transform: uppercase; | |
} | |
&::before { | |
position: absolute; | |
left: 0; | |
display: inline-block; | |
width: 100%; | |
content: attr(data-content); | |
color: rgba(128,128,128,0.25); | |
text-align: center; | |
text-transform: uppercase; | |
} | |
} | |
} | |
.days { | |
a { | |
line-height: 2; | |
&::after { | |
content: none; | |
position: absolute; | |
bottom: 0; right: .25rem; | |
display: inline-block; | |
width: 8px; height: 8px; | |
border-radius: 50%; | |
border: 1px solid $datepicker-bg; | |
background: $datepicker-selected-bg; | |
} | |
&.today::after { | |
content: ''; | |
background: $datepicker-today-indicator-bg; | |
} | |
time { | |
width: 2rem; height: 2rem; | |
border-radius: 50%; | |
line-height: 2.2; | |
letter-spacing: -.1em; | |
} | |
} | |
} | |
.time { | |
font-size: 2rem; | |
white-space: nowrap; | |
div { | |
position: relative; | |
display: inline-block; | |
width: 50%; | |
text-align: center; | |
&:first-child { | |
float: left; | |
&::after { | |
content: ':'; | |
position: absolute; | |
width: 1em; height: 1.5em; | |
right: -.5em; top: calc(50% - .25em); | |
line-height: 1.5; | |
} | |
} | |
&:last-child {float: right;} | |
} | |
label { | |
display: block; | |
width: 100%; | |
font-size: .5em; | |
margin: 0 0 1rem 0; | |
&:first-letter { | |
text-transform: uppercase; | |
} | |
} | |
button, input { | |
width: 2.5em; | |
line-height: 1; | |
margin: 0 auto; | |
font: inherit; | |
color: currentColor; | |
text-align: center; | |
float: none; | |
user-drag: none; | |
} | |
button { | |
display: block; | |
text-decoration: none; | |
&:first-of-type { | |
border-radius: $datepicker-border-radius $datepicker-border-radius 0 0; | |
} | |
&:last-of-type { | |
border-radius: 0 0 $datepicker-border-radius $datepicker-border-radius; | |
} | |
} | |
input { | |
font-variant-numeric: tabular-nums; | |
appearance: none; | |
/* autoprefixer: ignore next */ | |
-moz-appearance: textfield; | |
border: 0; | |
outline: 0; | |
&:focus { | |
} | |
&::-webkit-spin-button, | |
&::-webkit-inner-spin-button, | |
&::-webkit-outer-spin-button { | |
display: none; | |
margin: 0; | |
} | |
} | |
} | |
.year, .decade { | |
a { | |
line-height: 1; | |
width: 33.333%; | |
time { | |
width: 100%; height: 100%; | |
padding: 1.5rem .25rem; | |
font-size: .9em; | |
letter-spacing: 0; | |
border-radius: $datepicker-border-radius; | |
} | |
} | |
} | |
.decade { | |
a:last-child { margin: 0 33.333%; } | |
} | |
.days a, .year a, .decade a { | |
position: relative; | |
display: inline-block; | |
padding: 0; | |
color: currentColor; | |
text-align: center; | |
outline: none; | |
background: transparent!important; | |
&:hover time, &:focus time { | |
color: $datepicker-focus-color; | |
background-color: rgba($datepicker-focus-color, 0.1); | |
} | |
time { | |
display: inline-block; | |
font-variant-numeric: slashed-zero tabular-nums; | |
} | |
&.today, &.current { | |
time { | |
font-weight: bold; | |
background-color: $datepicker-today-bg; | |
color: $datepicker-today-color; | |
} | |
} | |
&[aria-selected] { | |
time { | |
background-color: $datepicker-selected-bg; | |
color: $datepicker-selected-color; | |
} | |
&.start, &.end { | |
&::before { | |
content: ''; | |
position: absolute; | |
height: 100%; width: 50%; | |
background-color: lighten($datepicker-selected-bg, 22); | |
z-index: -1; | |
} | |
} | |
&.start { | |
&::before { right: 0; } | |
time { border-radius:50% 0 0 50%; } | |
& ~ a { | |
background-color: lighten($datepicker-selected-bg, 22); | |
color: $datepicker-selected-bg; | |
} | |
} | |
&.end { | |
background-color: inherit!important; | |
&::before { left: 0; } | |
time { border-radius:0 50% 50% 0; } | |
& ~ a { | |
background-color: inherit; | |
color: inherit; | |
} | |
} | |
} | |
&[disabled]{ | |
border-radius: 0; | |
pointer-events: none; | |
cursor: not-allowed; | |
opacity: 0.5; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment