// Utility function
function Util () {};
/*
class manipulation functions
*/
Util.hasClass = function(el, className) {
if (el.classList) return el.classList.contains(className);
else return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
};
Util.addClass = function(el, className) {
var classList = className.split(' ');
if (el.classList) el.classList.add(classList[0]);
else if (!Util.hasClass(el, classList[0])) el.className += " " + classList[0];
if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' '));
};
Util.removeClass = function(el, className) {
var classList = className.split(' ');
if (el.classList) el.classList.remove(classList[0]);
else if(Util.hasClass(el, classList[0])) {
var reg = new RegExp('(\\s|^)' + classList[0] + '(\\s|$)');
el.className=el.className.replace(reg, ' ');
}
if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' '));
};
Util.toggleClass = function(el, className, bool) {
if(bool) Util.addClass(el, className);
else Util.removeClass(el, className);
};
Util.setAttributes = function(el, attrs) {
for(var key in attrs) {
el.setAttribute(key, attrs[key]);
}
};
/*
DOM manipulation
*/
Util.getChildrenByClassName = function(el, className) {
var children = el.children,
childrenByClass = [];
for (var i = 0; i < el.children.length; i++) {
if (Util.hasClass(el.children[i], className)) childrenByClass.push(el.children[i]);
}
return childrenByClass;
};
Util.is = function(elem, selector) {
if(selector.nodeType){
return elem === selector;
}
var qa = (typeof(selector) === 'string' ? document.querySelectorAll(selector) : selector),
length = qa.length,
returnArr = [];
while(length--){
if(qa[length] === elem){
return true;
}
}
return false;
};
/*
Animate height of an element
*/
Util.setHeight = function(start, to, element, duration, cb) {
var change = to - start,
currentTime = null;
var animateHeight = function(timestamp){
if (!currentTime) currentTime = timestamp;
var progress = timestamp - currentTime;
var val = parseInt((progress/duration)*change + start);
element.style.height = val+"px";
if(progress < duration) {
window.requestAnimationFrame(animateHeight);
} else {
cb();
}
};
//set the height of the element before starting animation -> fix bug on Safari
element.style.height = start+"px";
window.requestAnimationFrame(animateHeight);
};
/*
Smooth Scroll
*/
Util.scrollTo = function(final, duration, cb, scrollEl) {
var element = scrollEl || window;
var start = element.scrollTop || document.documentElement.scrollTop,
currentTime = null;
if(!scrollEl) start = window.scrollY || document.documentElement.scrollTop;
var animateScroll = function(timestamp){
if (!currentTime) currentTime = timestamp;
var progress = timestamp - currentTime;
if(progress > duration) progress = duration;
var val = Math.easeInOutQuad(progress, start, final-start, duration);
element.scrollTo(0, val);
if(progress < duration) {
window.requestAnimationFrame(animateScroll);
} else {
cb && cb();
}
};
window.requestAnimationFrame(animateScroll);
};
/*
Focus utility classes
*/
//Move focus to an element
Util.moveFocus = function (element) {
if( !element ) element = document.getElementsByTagName("body")[0];
element.focus();
if (document.activeElement !== element) {
element.setAttribute('tabindex','-1');
element.focus();
}
};
/*
Misc
*/
Util.getIndexInArray = function(array, el) {
return Array.prototype.indexOf.call(array, el);
};
Util.cssSupports = function(property, value) {
if('CSS' in window) {
return CSS.supports(property, value);
} else {
var jsProperty = property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase();});
return jsProperty in document.body.style;
}
};
// merge a set of user options into plugin defaults
// https://gomakethings.com/vanilla-javascript-version-of-jquery-extend/
Util.extend = function() {
// Variables
var extended = {};
var deep = false;
var i = 0;
var length = arguments.length;
// Check if a deep merge
if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
deep = arguments[0];
i++;
}
// Merge the object into the extended object
var merge = function (obj) {
for ( var prop in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) {
// If deep merge and property is an object, merge properties
if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) {
extended[prop] = extend( true, extended[prop], obj[prop] );
} else {
extended[prop] = obj[prop];
}
}
}
};
// Loop through each object and conduct a merge
for ( ; i < length; i++ ) {
var obj = arguments[i];
merge(obj);
}
return extended;
};
// Check if Reduced Motion is enabled
Util.osHasReducedMotion = function() {
if(!window.matchMedia) return false;
var matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)');
if(matchMediaObj) return matchMediaObj.matches;
return false; // return false if not supported
};
/*
Polyfills
*/
//Closest() method
if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function(s) {
var el = this;
if (!document.documentElement.contains(el)) return null;
do {
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}
//Custom Event() constructor
if ( typeof window.CustomEvent !== "function" ) {
function CustomEvent ( 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;
window.CustomEvent = CustomEvent;
}
/*
Animation curves
*/
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
};
Math.easeInQuart = function (t, b, c, d) {
t /= d;
return c*t*t*t*t + b;
};
Math.easeOutQuart = function (t, b, c, d) {
t /= d;
t--;
return -c * (t*t*t*t - 1) + b;
};
Math.easeInOutQuart = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t*t*t + b;
t -= 2;
return -c/2 * (t*t*t*t - 2) + b;
};
Math.easeOutElastic = function (t, b, c, d) {
var s=1.70158;var p=d*0.7;var a=c;
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
if (a < Math.abs(c)) { a=c; var s=p/4; }
else var s = p/(2*Math.PI) * Math.asin (c/a);
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
};
/* JS Utility Classes */
(function() {
// make focus ring visible only for keyboard navigation (i.e., tab key)
var focusTab = document.getElementsByClassName('js-tab-focus');
function detectClick() {
if(focusTab.length > 0) {
resetFocusTabs(false);
window.addEventListener('keydown', detectTab);
}
window.removeEventListener('mousedown', detectClick);
};
function detectTab(event) {
if(event.keyCode !== 9) return;
resetFocusTabs(true);
window.removeEventListener('keydown', detectTab);
window.addEventListener('mousedown', detectClick);
};
function resetFocusTabs(bool) {
var outlineStyle = bool ? '' : 'none';
for(var i = 0; i < focusTab.length; i++) {
focusTab[i].style.setProperty('outline', outlineStyle);
}
};
window.addEventListener('mousedown', detectClick);
}());
// File#: _1_anim-menu-btn
// Usage: codyhouse.co/license
(function() {
var menuBtns = document.getElementsByClassName('js-anim-menu-btn');
if( menuBtns.length > 0 ) {
for(var i = 0; i < menuBtns.length; i++) {(function(i){
initMenuBtn(menuBtns[i]);
})(i);}
function initMenuBtn(btn) {
btn.addEventListener('click', function(event){
event.preventDefault();
var status = !Util.hasClass(btn, 'anim-menu-btn--state-b');
Util.toggleClass(btn, 'anim-menu-btn--state-b', status);
// emit custom event
var event = new CustomEvent('anim-menu-btn-clicked', {detail: status});
btn.dispatchEvent(event);
});
};
}
}());
// File#: _1_choice-buttons
// Usage: codyhouse.co/license
(function() {
var ChoiceButton = function(element) {
this.element = element;
this.btns = this.element.getElementsByClassName('js-choice-btn');
this.inputs = getChoiceInput(this);
this.isRadio = this.inputs[0].type.toString() == 'radio';
resetCheckedStatus(this); // set initial classes
initChoiceButtonEvent(this); // add listeners
};
function getChoiceInput(element) { // store input elements in an object property
var inputs = [];
for(var i = 0; i < element.btns.length; i++) {
inputs.push(element.btns[i].getElementsByTagName('input')[0]);
}
return inputs;
};
function initChoiceButtonEvent(choiceBtn) {
choiceBtn.element.addEventListener('click', function(event){ // update status on click
if(Util.getIndexInArray(choiceBtn.inputs, event.target) > -1) return; // triggered by change in input element -> will be detected by the 'change' event
var selectedBtn = event.target.closest('.js-choice-btn');
if(!selectedBtn) return;
var index = Util.getIndexInArray(choiceBtn.btns, selectedBtn);
if(choiceBtn.isRadio && choiceBtn.inputs[index].checked) { // radio input already checked
choiceBtn.inputs[index].focus(); // move focus to input element
return;
}
choiceBtn.inputs[index].checked = !choiceBtn.inputs[index].checked;
choiceBtn.inputs[index].dispatchEvent(new CustomEvent('change')); // trigger change event
choiceBtn.inputs[index].focus(); // move focus to input element
});
for(var i = 0; i < choiceBtn.btns.length; i++) {(function(i){ // change + focus events
choiceBtn.inputs[i].addEventListener('change', function(event){
choiceBtn.isRadio ? resetCheckedStatus(choiceBtn) : resetSingleStatus(choiceBtn, i);
});
choiceBtn.inputs[i].addEventListener('focus', function(event){
resetFocusStatus(choiceBtn, i, true);
});
choiceBtn.inputs[i].addEventListener('blur', function(event){
resetFocusStatus(choiceBtn, i, false);
});
})(i);}
};
function resetCheckedStatus(choiceBtn) {
for(var i = 0; i < choiceBtn.btns.length; i++) {
resetSingleStatus(choiceBtn, i);
}
};
function resetSingleStatus(choiceBtn, index) { // toggle .choice-btn--checked class
Util.toggleClass(choiceBtn.btns[index], 'choice-btn--checked', choiceBtn.inputs[index].checked);
};
function resetFocusStatus(choiceBtn, index, bool) { // toggle .choice-btn--focus class
Util.toggleClass(choiceBtn.btns[index], 'choice-btn--focus', bool);
};
//initialize the ChoiceButtons objects
window.choiceButtonInit = function(){
var choiceButton = document.getElementsByClassName('js-choice-btns');
if( choiceButton.length > 0 ) {
for( var i = 0; i < choiceButton.length; i++) {
(function(i){new ChoiceButton(choiceButton[i]);})(i);
}
};
}
}());
// File#: _1_date-picker
// Usage: codyhouse.co/license
(function() {
var DatePicker = function(opts) {
this.options = Util.extend(DatePicker.defaults , opts);
this.element = this.options.element;
this.input = this.element.getElementsByClassName('js-date-input__text')[0];
this.trigger = this.element.getElementsByClassName('js-date-input__trigger')[0];
this.triggerLabel = this.trigger.getAttribute('aria-label');
this.datePicker = this.element.getElementsByClassName('js-date-picker')[0];
this.body = this.datePicker.getElementsByClassName('js-date-picker__dates')[0];
this.navigation = this.datePicker.getElementsByClassName('js-date-picker__month-nav')[0];
this.heading = this.datePicker.getElementsByClassName('js-date-picker__month-label')[0];
this.pickerVisible = false;
// date format
this.dateIndexes = getDateIndexes(this); // store indexes of date parts (d, m, y)
// set initial date
resetCalendar(this);
// selected date
this.dateSelected = false;
this.selectedDay = false;
this.selectedMonth = false;
this.selectedYear = false;
// focus trap
this.firstFocusable = false;
this.lastFocusable = false;
// date value - for custom control variation
this.dateValueEl = this.element.getElementsByClassName('js-date-input__value');
if(this.dateValueEl.length > 0) {
this.dateValueLabelInit = this.dateValueEl[0].textContent; // initial input value
}
initCalendarAria(this);
initCalendarEvents(this);
// place picker according to available space
placeCalendar(this);
};
DatePicker.prototype.showCalendar = function() {
showCalendar(this);
};
DatePicker.prototype.showNextMonth = function() {
showNext(this, true);
};
DatePicker.prototype.showPrevMonth = function() {
showPrev(this, true);
};
function initCalendarAria(datePicker) {
// reset calendar button label
resetLabelCalendarTrigger(datePicker);
if(datePicker.dateValueEl.length > 0) {
resetCalendar(datePicker);
resetLabelCalendarValue(datePicker);
}
// create a live region used to announce new month selection to SR
var srLiveReagion = document.createElement('div');
srLiveReagion.setAttribute('aria-live', 'polite');
Util.addClass(srLiveReagion, 'sr-only js-date-input__sr-live');
datePicker.element.appendChild(srLiveReagion);
datePicker.srLiveReagion = datePicker.element.getElementsByClassName('js-date-input__sr-live')[0];
};
function initCalendarEvents(datePicker) {
datePicker.input.addEventListener('focus', function(event){
toggleCalendar(datePicker, true); // toggle calendar when focus is on input
});
if(datePicker.trigger) {
datePicker.trigger.addEventListener('click', function(event){ // open calendar when clicking on calendar button
event.preventDefault();
datePicker.pickerVisible = false;
toggleCalendar(datePicker);
datePicker.trigger.setAttribute('aria-expanded', 'true');
});
}
// select a date inside the date picker
datePicker.body.addEventListener('click', function(event){
event.preventDefault();
var day = event.target.closest('button');
if(day) {
datePicker.dateSelected = true;
datePicker.selectedDay = day.innerText;
datePicker.selectedMonth = datePicker.currentMonth;
datePicker.selectedYear = datePicker.currentYear;
setInputValue(datePicker);
datePicker.input.focus(); // focus on the input element and close picker
resetLabelCalendarTrigger(datePicker);
resetLabelCalendarValue(datePicker);
}
});
// navigate using month nav
datePicker.navigation.addEventListener('click', function(event){
event.preventDefault();
var btn = event.target.closest('.js-date-picker__month-nav-btn');
if(btn) {
Util.hasClass(btn, 'js-date-picker__month-nav-btn--prev') ? showPrev(datePicker, true) : showNext(datePicker, true);
}
});
// hide calendar
window.addEventListener('keydown', function(event){ // close calendar on esc
if(event.keyCode && event.keyCode == 27 || event.key && event.key.toLowerCase() == 'escape') {
if(document.activeElement.closest('.js-date-picker')) {
datePicker.input.focus(); //if focus is inside the calendar -> move the focus to the input element
} else { // do not move focus -> only close calendar
hideCalendar(datePicker);
}
}
});
window.addEventListener('click', function(event){
if(!event.target.closest('.js-date-picker') && !event.target.closest('.js-date-input') && datePicker.pickerVisible) {
hideCalendar(datePicker);
}
});
// navigate through days of calendar
datePicker.body.addEventListener('keydown', function(event){
var day = datePicker.currentDay;
if(event.keyCode && event.keyCode == 40 || event.key && event.key.toLowerCase() == 'arrowdown') {
day = day + 7;
resetDayValue(day, datePicker);
} else if(event.keyCode && event.keyCode == 39 || event.key && event.key.toLowerCase() == 'arrowright') {
day = day + 1;
resetDayValue(day, datePicker);
} else if(event.keyCode && event.keyCode == 37 || event.key && event.key.toLowerCase() == 'arrowleft') {
day = day - 1;
resetDayValue(day, datePicker);
} else if(event.keyCode && event.keyCode == 38 || event.key && event.key.toLowerCase() == 'arrowup') {
day = day - 7;
resetDayValue(day, datePicker);
} else if(event.keyCode && event.keyCode == 35 || event.key && event.key.toLowerCase() == 'end') { // move focus to last day of week
event.preventDefault();
day = day + 6 - getDayOfWeek(datePicker.currentYear, datePicker.currentMonth, day);
resetDayValue(day, datePicker);
} else if(event.keyCode && event.keyCode == 36 || event.key && event.key.toLowerCase() == 'home') { // move focus to first day of week
event.preventDefault();
day = day - getDayOfWeek(datePicker.currentYear, datePicker.currentMonth, day);
resetDayValue(day, datePicker);
} else if(event.keyCode && event.keyCode == 34 || event.key && event.key.toLowerCase() == 'pagedown') {
event.preventDefault();
showNext(datePicker); // show next month
} else if(event.keyCode && event.keyCode == 33 || event.key && event.key.toLowerCase() == 'pageup') {
event.preventDefault();
showPrev(datePicker); // show prev month
}
});
// trap focus inside calendar
datePicker.datePicker.addEventListener('keydown', function(event){
if( event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab' ) {
//trap focus inside modal
trapFocus(event, datePicker);
}
});
datePicker.input.addEventListener('keydown', function(event){
if(event.keyCode && event.keyCode == 13 || event.key && event.key.toLowerCase() == 'enter') {
// update calendar on input enter
resetCalendar(datePicker);
resetLabelCalendarTrigger(datePicker);
resetLabelCalendarValue(datePicker);
hideCalendar(datePicker);
} else if(event.keyCode && event.keyCode == 40 || event.key && event.key.toLowerCase() == 'arrowdown' && datePicker.pickerVisible) { // move focus to calendar using arrow down
datePicker.body.querySelector('button[tabindex="0"]').focus();
};
});
};
function getCurrentDay(date) {
return (date)
? getDayFromDate(date)
: new Date().getDate();
};
function getCurrentMonth(date) {
return (date)
? getMonthFromDate(date)
: new Date().getMonth();
};
function getCurrentYear(date) {
return (date)
? getYearFromDate(date)
: new Date().getFullYear();
};
function getDayFromDate(date) {
var day = parseInt(date.split('-')[2]);
return isNaN(day) ? getCurrentDay(false) : day;
};
function getMonthFromDate(date) {
var month = parseInt(date.split('-')[1]) - 1;
return isNaN(month) ? getCurrentMonth(false) : month;
};
function getYearFromDate(date) {
var year = parseInt(date.split('-')[0]);
return isNaN(year) ? getCurrentYear(false) : year;
};
function showNext(datePicker, bool) {
// show next month
datePicker.currentYear = (datePicker.currentMonth === 11) ? datePicker.currentYear + 1 : datePicker.currentYear;
datePicker.currentMonth = (datePicker.currentMonth + 1) % 12;
datePicker.currentDay = checkDayInMonth(datePicker);
showCalendar(datePicker, bool);
datePicker.srLiveReagion.textContent = datePicker.options.months[datePicker.currentMonth] + ' ' + datePicker.currentYear;
};
function showPrev(datePicker, bool) {
// show prev month
datePicker.currentYear = (datePicker.currentMonth === 0) ? datePicker.currentYear - 1 : datePicker.currentYear;
datePicker.currentMonth = (datePicker.currentMonth === 0) ? 11 : datePicker.currentMonth - 1;
datePicker.currentDay = checkDayInMonth(datePicker);
showCalendar(datePicker, bool);
datePicker.srLiveReagion.textContent = datePicker.options.months[datePicker.currentMonth] + ' ' + datePicker.currentYear;
};
function checkDayInMonth(datePicker) {
return (datePicker.currentDay > daysInMonth(datePicker.currentYear, datePicker.currentMonth)) ? 1 : datePicker.currentDay;
};
function daysInMonth(year, month) {
return 32 - new Date(year, month, 32).getDate();
};
function resetCalendar(datePicker) {
var currentDate = false,
selectedDate = datePicker.input.value;
datePicker.dateSelected = false;
if( selectedDate != '') {
var date = getDateFromInput(datePicker);
datePicker.dateSelected = true;
currentDate = date;
}
datePicker.currentDay = getCurrentDay(currentDate);
datePicker.currentMonth = getCurrentMonth(currentDate);
datePicker.currentYear = getCurrentYear(currentDate);
datePicker.selectedDay = datePicker.dateSelected ? datePicker.currentDay : false;
datePicker.selectedMonth = datePicker.dateSelected ? datePicker.currentMonth : false;
datePicker.selectedYear = datePicker.dateSelected ? datePicker.currentYear : false;
};
function showCalendar(datePicker, bool) {
// show calendar element
var firstDay = getDayOfWeek(datePicker.currentYear, datePicker.currentMonth, '01');
datePicker.body.innerHTML = '';
datePicker.heading.innerHTML = datePicker.options.months[datePicker.currentMonth] + ' ' + datePicker.currentYear;
// creating all cells
var date = 1,
calendar = '';
for (var i = 0; i < 6; i++) {
for (var j = 0; j < 7; j++) {
if (i === 0 && j < firstDay) {
calendar = calendar + '
';
} else if (date > daysInMonth(datePicker.currentYear, datePicker.currentMonth)) {
break;
} else {
var classListDate = '',
tabindexValue = '-1';
if (date === datePicker.currentDay) {
tabindexValue = '0';
}
if(!datePicker.dateSelected && getCurrentMonth() == datePicker.currentMonth && getCurrentYear() == datePicker.currentYear && date == getCurrentDay()){
classListDate = classListDate+' date-picker__date--today'
}
if (datePicker.dateSelected && date === datePicker.selectedDay && datePicker.currentYear === datePicker.selectedYear && datePicker.currentMonth === datePicker.selectedMonth) {
classListDate = classListDate+' date-picker__date--selected';
}
calendar = calendar + '';
date++;
}
}
}
datePicker.body.innerHTML = calendar; // appending days into calendar body
// show calendar
if(!datePicker.pickerVisible) Util.addClass(datePicker.datePicker, 'date-picker--is-visible');
datePicker.pickerVisible = true;
// if bool is false, move focus to calendar day
if(!bool) datePicker.body.querySelector('button[tabindex="0"]').focus();
// store first/last focusable elements
getFocusableElements(datePicker);
//place calendar
placeCalendar(datePicker);
};
function hideCalendar(datePicker) {
Util.removeClass(datePicker.datePicker, 'date-picker--is-visible');
datePicker.pickerVisible = false;
// reset first/last focusable
datePicker.firstFocusable = false;
datePicker.lastFocusable = false;
// reset trigger aria-expanded attribute
if(datePicker.trigger) datePicker.trigger.setAttribute('aria-expanded', 'false');
};
function toggleCalendar(datePicker, bool) {
if(!datePicker.pickerVisible) {
resetCalendar(datePicker);
showCalendar(datePicker, bool);
} else {
hideCalendar(datePicker);
}
};
function getDayOfWeek(year, month, day) {
var weekDay = (new Date(year, month, day)).getDay() - 1;
if(weekDay < 0) weekDay = 6;
return weekDay;
};
function getDateIndexes(datePicker) {
var dateFormat = datePicker.options.dateFormat.toLowerCase().replace(/-/g, '');
return [dateFormat.indexOf('d'), dateFormat.indexOf('m'), dateFormat.indexOf('y')];
};
function setInputValue(datePicker) {
datePicker.input.value = getDateForInput(datePicker);
};
function getDateForInput(datePicker) {
var dateArray = [];
dateArray[datePicker.dateIndexes[0]] = getReadableDate(datePicker.selectedDay);
dateArray[datePicker.dateIndexes[1]] = getReadableDate(datePicker.selectedMonth+1);
dateArray[datePicker.dateIndexes[2]] = datePicker.selectedYear;
return dateArray[0]+datePicker.options.dateSeparator+dateArray[1]+datePicker.options.dateSeparator+dateArray[2];
};
function getDateFromInput(datePicker) {
var dateArray = datePicker.input.value.split(datePicker.options.dateSeparator);
return dateArray[datePicker.dateIndexes[2]]+'-'+dateArray[datePicker.dateIndexes[1]]+'-'+dateArray[datePicker.dateIndexes[0]];
};
function getReadableDate(date) {
return (date < 10) ? '0'+date : date;
};
function resetDayValue(day, datePicker) {
var totDays = daysInMonth(datePicker.currentYear, datePicker.currentMonth);
if( day > totDays) {
datePicker.currentDay = day - totDays;
showNext(datePicker, false);
} else if(day < 1) {
var newMonth = datePicker.currentMonth == 0 ? 11 : datePicker.currentMonth - 1;
datePicker.currentDay = daysInMonth(datePicker.currentYear, newMonth) + day;
showPrev(datePicker, false);
} else {
datePicker.currentDay = day;
datePicker.body.querySelector('button[tabindex="0"]').setAttribute('tabindex', '-1');
// set new tabindex to selected item
var buttons = datePicker.body.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].textContent == datePicker.currentDay) {
buttons[i].setAttribute('tabindex', '0');
buttons[i].focus();
break;
}
}
getFocusableElements(datePicker); // update first focusable/last focusable element
}
};
function resetLabelCalendarTrigger(datePicker) {
if(!datePicker.trigger) return;
// reset accessible label of the calendar trigger
(datePicker.selectedYear && datePicker.selectedMonth && datePicker.selectedDay)
? datePicker.trigger.setAttribute('aria-label', datePicker.triggerLabel+', selected date is '+ new Date(datePicker.selectedYear, datePicker.selectedMonth, datePicker.selectedDay).toDateString())
: datePicker.trigger.setAttribute('aria-label', datePicker.triggerLabel);
};
function resetLabelCalendarValue(datePicker) {
// this is used for the --custom-control variation -> there's a label that should be updated with the selected date
if(datePicker.dateValueEl.length < 1) return;
(datePicker.selectedYear && datePicker.selectedMonth && datePicker.selectedDay)
? datePicker.dateValueEl[0].textContent = getDateForInput(datePicker)
: datePicker.dateValueEl[0].textContent = datePicker.dateValueLabelInit;
};
function getFocusableElements(datePicker) {
var allFocusable = datePicker.datePicker.querySelectorAll('[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable], audio[controls], video[controls], summary');
getFirstFocusable(allFocusable, datePicker);
getLastFocusable(allFocusable, datePicker);
}
function getFirstFocusable(elements, datePicker) {
for(var i = 0; i < elements.length; i++) {
if( (elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length) && elements[i].getAttribute('tabindex') != '-1') {
datePicker.firstFocusable = elements[i];
return true;
}
}
};
function getLastFocusable(elements, datePicker) {
//get last visible focusable element inside the modal
for(var i = elements.length - 1; i >= 0; i--) {
if( (elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length) && elements[i].getAttribute('tabindex') != '-1' ) {
datePicker.lastFocusable = elements[i];
return true;
}
}
};
function trapFocus(event, datePicker) {
if( datePicker.firstFocusable == document.activeElement && event.shiftKey) {
//on Shift+Tab -> focus last focusable element when focus moves out of calendar
event.preventDefault();
datePicker.lastFocusable.focus();
}
if( datePicker.lastFocusable == document.activeElement && !event.shiftKey) {
//on Tab -> focus first focusable element when focus moves out of calendar
event.preventDefault();
datePicker.firstFocusable.focus();
}
};
function placeCalendar(datePicker) {
// reset position
datePicker.datePicker.style.left = '0px';
datePicker.datePicker.style.right = 'auto';
//check if you need to modify the calendar postion
var pickerBoundingRect = datePicker.datePicker.getBoundingClientRect();
if(pickerBoundingRect.right > window.innerWidth) {
datePicker.datePicker.style.left = 'auto';
datePicker.datePicker.style.right = '0px';
}
};
DatePicker.defaults = {
element : '',
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
dateFormat: 'd-m-y',
dateSeparator: '/'
};
window.DatePicker = DatePicker;
var datePicker = document.getElementsByClassName('js-date-input'),
flexSupported = Util.cssSupports('align-items', 'stretch');
if( datePicker.length > 0 ) {
for( var i = 0; i < datePicker.length; i++) {(function(i){
if(!flexSupported) {
Util.addClass(datePicker[i], 'date-input--hide-calendar');
return;
}
var opts = {element: datePicker[i]};
if(datePicker[i].getAttribute('data-date-format')) {
opts.dateFormat = datePicker[i].getAttribute('data-date-format');
}
if(datePicker[i].getAttribute('data-date-separator')) {
opts.dateSeparator = datePicker[i].getAttribute('data-date-separator');
}
if(datePicker[i].getAttribute('data-months')) {
opts.months = datePicker[i].getAttribute('data-months').split(',').map(function(item) {return item.trim();});
}
new DatePicker(opts);
})(i);}
}
}());
// File#: _1_diagonal-movement
// Usage: codyhouse.co/license
/*
Modified version of the jQuery-menu-aim plugin
https://github.com/kamens/jQuery-menu-aim
- Replaced jQuery with Vanilla JS
- Minor changes
*/
(function() {
var menuAim = function(opts) {
init(opts);
};
window.menuAim = menuAim;
function init(opts) {
var activeRow = null,
mouseLocs = [],
lastDelayLoc = null,
timeoutId = null,
options = Util.extend({
menu: '',
rows: false, //if false, get direct children - otherwise pass nodes list
submenuSelector: "*",
submenuDirection: "right",
tolerance: 75, // bigger = more forgivey when entering submenu
enter: function(){},
exit: function(){},
activate: function(){},
deactivate: function(){},
exitMenu: function(){}
}, opts),
menu = options.menu;
var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track
DELAY = 300; // ms delay when user appears to be entering submenu
/**
* Keep track of the last few locations of the mouse.
*/
var mousemoveDocument = function(e) {
mouseLocs.push({x: e.pageX, y: e.pageY});
if (mouseLocs.length > MOUSE_LOCS_TRACKED) {
mouseLocs.shift();
}
};
/**
* Cancel possible row activations when leaving the menu entirely
*/
var mouseleaveMenu = function() {
if (timeoutId) {
clearTimeout(timeoutId);
}
// If exitMenu is supplied and returns true, deactivate the
// currently active row on menu exit.
if (options.exitMenu(this)) {
if (activeRow) {
options.deactivate(activeRow);
}
activeRow = null;
}
};
/**
* Trigger a possible row activation whenever entering a new row.
*/
var mouseenterRow = function() {
if (timeoutId) {
// Cancel any previous activation delays
clearTimeout(timeoutId);
}
options.enter(this);
possiblyActivate(this);
},
mouseleaveRow = function() {
options.exit(this);
};
/*
* Immediately activate a row if the user clicks on it.
*/
var clickRow = function() {
activate(this);
};
/**
* Activate a menu row.
*/
var activate = function(row) {
if (row == activeRow) {
return;
}
if (activeRow) {
options.deactivate(activeRow);
}
options.activate(row);
activeRow = row;
};
/**
* Possibly activate a menu row. If mouse movement indicates that we
* shouldn't activate yet because user may be trying to enter
* a submenu's content, then delay and check again later.
*/
var possiblyActivate = function(row) {
var delay = activationDelay();
if (delay) {
timeoutId = setTimeout(function() {
possiblyActivate(row);
}, delay);
} else {
activate(row);
}
};
/**
* Return the amount of time that should be used as a delay before the
* currently hovered row is activated.
*
* Returns 0 if the activation should happen immediately. Otherwise,
* returns the number of milliseconds that should be delayed before
* checking again to see if the row should be activated.
*/
var activationDelay = function() {
if (!activeRow || !Util.is(activeRow, options.submenuSelector)) {
// If there is no other submenu row already active, then
// go ahead and activate immediately.
return 0;
}
function getOffset(element) {
var rect = element.getBoundingClientRect();
return { top: rect.top + window.pageYOffset, left: rect.left + window.pageXOffset };
};
var offset = getOffset(menu),
upperLeft = {
x: offset.left,
y: offset.top - options.tolerance
},
upperRight = {
x: offset.left + menu.offsetWidth,
y: upperLeft.y
},
lowerLeft = {
x: offset.left,
y: offset.top + menu.offsetHeight + options.tolerance
},
lowerRight = {
x: offset.left + menu.offsetWidth,
y: lowerLeft.y
},
loc = mouseLocs[mouseLocs.length - 1],
prevLoc = mouseLocs[0];
if (!loc) {
return 0;
}
if (!prevLoc) {
prevLoc = loc;
}
if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || prevLoc.y < offset.top || prevLoc.y > lowerRight.y) {
// If the previous mouse location was outside of the entire
// menu's bounds, immediately activate.
return 0;
}
if (lastDelayLoc && loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) {
// If the mouse hasn't moved since the last time we checked
// for activation status, immediately activate.
return 0;
}
// Detect if the user is moving towards the currently activated
// submenu.
//
// If the mouse is heading relatively clearly towards
// the submenu's content, we should wait and give the user more
// time before activating a new row. If the mouse is heading
// elsewhere, we can immediately activate a new row.
//
// We detect this by calculating the slope formed between the
// current mouse location and the upper/lower right points of
// the menu. We do the same for the previous mouse location.
// If the current mouse location's slopes are
// increasing/decreasing appropriately compared to the
// previous's, we know the user is moving toward the submenu.
//
// Note that since the y-axis increases as the cursor moves
// down the screen, we are looking for the slope between the
// cursor and the upper right corner to decrease over time, not
// increase (somewhat counterintuitively).
function slope(a, b) {
return (b.y - a.y) / (b.x - a.x);
};
var decreasingCorner = upperRight,
increasingCorner = lowerRight;
// Our expectations for decreasing or increasing slope values
// depends on which direction the submenu opens relative to the
// main menu. By default, if the menu opens on the right, we
// expect the slope between the cursor and the upper right
// corner to decrease over time, as explained above. If the
// submenu opens in a different direction, we change our slope
// expectations.
if (options.submenuDirection == "left") {
decreasingCorner = lowerLeft;
increasingCorner = upperLeft;
} else if (options.submenuDirection == "below") {
decreasingCorner = lowerRight;
increasingCorner = lowerLeft;
} else if (options.submenuDirection == "above") {
decreasingCorner = upperLeft;
increasingCorner = upperRight;
}
var decreasingSlope = slope(loc, decreasingCorner),
increasingSlope = slope(loc, increasingCorner),
prevDecreasingSlope = slope(prevLoc, decreasingCorner),
prevIncreasingSlope = slope(prevLoc, increasingCorner);
if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) {
// Mouse is moving from previous location towards the
// currently activated submenu. Delay before activating a
// new menu row, because user may be moving into submenu.
lastDelayLoc = loc;
return DELAY;
}
lastDelayLoc = null;
return 0;
};
/**
* Hook up initial menu events
*/
menu.addEventListener('mouseleave', mouseleaveMenu);
var rows = (options.rows) ? options.rows : menu.children;
if(rows.length > 0) {
for(var i = 0; i < rows.length; i++) {(function(i){
rows[i].addEventListener('mouseenter', mouseenterRow);
rows[i].addEventListener('mouseleave', mouseleaveRow);
rows[i].addEventListener('click', clickRow);
})(i);}
}
document.addEventListener('mousemove', function(event){
(!window.requestAnimationFrame) ? mousemoveDocument(event) : window.requestAnimationFrame(function(){mousemoveDocument(event);});
});
};
}());
// File#: _1_filter-navigation
// Usage: codyhouse.co/license
(function() {
var FilterNav = function(element) {
this.element = element;
this.wrapper = this.element.getElementsByClassName('js-filter-nav__wrapper')[0];
this.nav = this.element.getElementsByClassName('js-filter-nav__nav')[0];
this.list = this.nav.getElementsByClassName('js-filter-nav__list')[0];
this.control = this.element.getElementsByClassName('js-filter-nav__control')[0];
this.modalClose = this.element.getElementsByClassName('js-filter-nav__close-btn')[0];
this.placeholder = this.element.getElementsByClassName('js-filter-nav__placeholder')[0];
this.marker = this.element.getElementsByClassName('js-filter-nav__marker');
this.layout = 'expanded';
initFilterNav(this);
};
function initFilterNav(element) {
checkLayout(element); // init layout
if(element.layout == 'expanded') placeMarker(element);
element.element.addEventListener('update-layout', function(event){ // on resize - modify layout
checkLayout(element);
});
// update selected item
element.wrapper.addEventListener('click', function(event){
var newItem = event.target.closest('.js-filter-nav__btn');
if(newItem) {
updateCurrentItem(element, newItem);
return;
}
// close modal list - mobile version only
if(Util.hasClass(event.target, 'js-filter-nav__wrapper') || event.target.closest('.js-filter-nav__close-btn')) toggleModalList(element, false);
});
// open modal list - mobile version only
element.control.addEventListener('click', function(event){
toggleModalList(element, true);
});
// listen for key events
window.addEventListener('keyup', function(event){
// listen for esc key
if( (event.keyCode && event.keyCode == 27) || (event.key && event.key.toLowerCase() == 'escape' )) {
// close navigation on mobile if open
if(element.control.getAttribute('aria-expanded') == 'true' && isVisible(element.control)) {
toggleModalList(element, false);
}
}
// listen for tab key
if( (event.keyCode && event.keyCode == 9) || (event.key && event.key.toLowerCase() == 'tab' )) {
// close navigation on mobile if open when nav loses focus
if(element.control.getAttribute('aria-expanded') == 'true' && isVisible(element.control) && !document.activeElement.closest('.js-filter-nav__wrapper')) toggleModalList(element, false);
}
});
};
function updateCurrentItem(element, btn) {
if(btn.getAttribute('aria-current') == 'true') {
toggleModalList(element, false);
return;
}
var activeBtn = element.wrapper.querySelector('[aria-current]');
if(activeBtn) activeBtn.removeAttribute('aria-current');
btn.setAttribute('aria-current', 'true');
// update trigger label on selection (visible on mobile only)
element.placeholder.textContent = btn.textContent;
toggleModalList(element, false);
if(element.layout == 'expanded') placeMarker(element);
};
function toggleModalList(element, bool) {
element.control.setAttribute('aria-expanded', bool);
Util.toggleClass(element.wrapper, 'filter-nav__wrapper--is-visible', bool);
if(bool) {
element.nav.querySelectorAll('[href], button:not([disabled])')[0].focus();
} else if(isVisible(element.control)) {
element.control.focus();
}
};
function isVisible(element) {
return (element.offsetWidth || element.offsetHeight || element.getClientRects().length);
};
function checkLayout(element) {
if(element.layout == 'expanded' && switchToCollapsed(element)) { // check if there's enough space
element.layout = 'collapsed';
Util.removeClass(element.element, 'filter-nav--expanded');
Util.addClass(element.element, 'filter-nav--collapsed');
Util.removeClass(element.modalClose, 'is-hidden');
Util.removeClass(element.control, 'is-hidden');
} else if(element.layout == 'collapsed' && switchToExpanded(element)) {
element.layout = 'expanded';
Util.addClass(element.element, 'filter-nav--expanded');
Util.removeClass(element.element, 'filter-nav--collapsed');
Util.addClass(element.modalClose, 'is-hidden');
Util.addClass(element.control, 'is-hidden');
}
// place background element
if(element.layout == 'expanded') placeMarker(element);
};
function switchToCollapsed(element) {
return element.nav.scrollWidth > element.nav.offsetWidth;
};
function switchToExpanded(element) {
element.element.style.visibility = 'hidden';
Util.addClass(element.element, 'filter-nav--expanded');
Util.removeClass(element.element, 'filter-nav--collapsed');
var switchLayout = element.nav.scrollWidth <= element.nav.offsetWidth;
Util.removeClass(element.element, 'filter-nav--expanded');
Util.addClass(element.element, 'filter-nav--collapsed');
element.element.style.visibility = 'visible';
return switchLayout;
};
function placeMarker(element) {
var activeElement = element.wrapper.querySelector('.js-filter-nav__btn[aria-current="true"]');
if(element.marker.length == 0 || !activeElement ) return;
element.marker[0].style.width = activeElement.offsetWidth+'px';
element.marker[0].style.transform = 'translateX('+(activeElement.getBoundingClientRect().left - element.list.getBoundingClientRect().left)+'px)';
};
var filterNav = document.getElementsByClassName('js-filter-nav');
if(filterNav.length > 0) {
var filterNavArray = [];
for(var i = 0; i < filterNav.length; i++) {
filterNavArray.push(new FilterNav(filterNav[i]));
}
var resizingId = false,
customEvent = new CustomEvent('update-layout');
window.addEventListener('resize', function() {
clearTimeout(resizingId);
resizingId = setTimeout(doneResizing, 100);
});
// wait for font to be loaded
document.fonts.onloadingdone = function (fontFaceSetEvent) {
doneResizing();
};
function doneResizing() {
for( var i = 0; i < filterNavArray.length; i++) {
(function(i){filterNavArray[i].element.dispatchEvent(customEvent)})(i);
};
};
}
}());
// File#: _1_filter
// Usage: codyhouse.co/license
(function() {
var Filter = function(opts) {
this.options = Util.extend(Filter.defaults , opts); // used to store custom filter/sort functions
this.element = this.options.element;
this.elementId = this.element.getAttribute('id');
this.items = this.element.querySelectorAll('.js-filter__item');
this.controllers = document.querySelectorAll('[aria-controls="'+this.elementId+'"]'); // controllers wrappers
this.fallbackMessage = document.querySelector('[data-fallback-gallery-id="'+this.elementId+'"]');
this.filterString = []; // combination of different filter values
this.sortingString = ''; // sort value - will include order and type of argument (e.g., number or string)
// store info about sorted/filtered items
this.filterList = []; // list of boolean for each this.item -> true if still visible , otherwise false
this.sortingList = []; // list of new ordered this.item -> each element is [item, originalIndex]
// store grid info for animation
this.itemsGrid = []; // grid coordinates
this.itemsInitPosition = []; // used to store coordinates of this.items before animation
this.itemsIterPosition = []; // used to store coordinates of this.items before animation - intermediate state
this.itemsFinalPosition = []; // used to store coordinates of this.items after filtering
// animation off
this.animateOff = this.element.getAttribute('data-filter-animation') == 'off';
// used to update this.itemsGrid on resize
this.resizingId = false;
// default acceleration style - improve animation
this.accelerateStyle = 'will-change: transform, opacity; transform: translateZ(0); backface-visibility: hidden;';
// handle multiple changes
this.animating = false;
this.reanimate = false;
initFilter(this);
};
function initFilter(filter) {
resetFilterSortArray(filter, true, true); // init array filter.filterList/filter.sortingList
createGridInfo(filter); // store grid coordinates in filter.itemsGrid
initItemsOrder(filter); // add data-orders so that we can reset the sorting
// events handling - filter update
for(var i = 0; i < filter.controllers.length; i++) {
filter.filterString[i] = ''; // reset filtering
// get proper filter/sorting string based on selected controllers
(function(i){
filter.controllers[i].addEventListener('change', function(event) {
if(event.target.tagName.toLowerCase() == 'select') { // select elements
(!event.target.getAttribute('data-filter'))
? setSortingString(filter, event.target.value, event.target.options[event.target.selectedIndex])
: setFilterString(filter, i, 'select');
} else if(event.target.tagName.toLowerCase() == 'input' && (event.target.getAttribute('type') == 'radio' || event.target.getAttribute('type') == 'checkbox') ) { // input (radio/checkboxed) elements
(!event.target.getAttribute('data-filter'))
? setSortingString(filter, event.target.getAttribute('data-sort'), event.target)
: setFilterString(filter, i, 'input');
} else {
// generic inout element
(!filter.controllers[i].getAttribute('data-filter'))
? setSortingString(filter, filter.controllers[i].getAttribute('data-sort'), filter.controllers[i])
: setFilterString(filter, i, 'custom');
}
updateFilterArray(filter);
});
filter.controllers[i].addEventListener('click', function(event) { // retunr if target is select/input elements
var filterEl = event.target.closest('[data-filter]');
var sortEl = event.target.closest('[data-sort]');
if(!filterEl && !sortEl) return;
if(filterEl && ( filterEl.tagName.toLowerCase() == 'input' || filterEl.tagName.toLowerCase() == 'select')) return;
if(sortEl && (sortEl.tagName.toLowerCase() == 'input' || sortEl.tagName.toLowerCase() == 'select')) return;
if(sortEl && Util.hasClass(sortEl, 'js-filter__custom-control')) return;
if(filterEl && Util.hasClass(filterEl, 'js-filter__custom-control')) return;
// this will be executed only for a list of buttons -> no inputs
event.preventDefault();
resetControllersList(filter, i, filterEl, sortEl);
sortEl
? setSortingString(filter, sortEl.getAttribute('data-sort'), sortEl)
: setFilterString(filter, i, 'button');
updateFilterArray(filter);
});
})(i);
}
// handle resize - update grid coordinates in filter.itemsGrid
window.addEventListener('resize', function() {
clearTimeout(filter.resizingId);
filter.resizingId = setTimeout(function(){createGridInfo(filter)}, 300);
});
// check if there are filters/sorting values already set
checkInitialFiltering(filter);
// reset filtering results if filter selection was changed by an external control (e.g., form reset)
filter.element.addEventListener('update-filter-results', function(event){
// reset filters first
for(var i = 0; i < filter.controllers.length; i++) filter.filterString[i] = '';
filter.sortingString = '';
checkInitialFiltering(filter);
});
};
function checkInitialFiltering(filter) {
for(var i = 0; i < filter.controllers.length; i++) { // check if there's a selected option
// buttons list
var selectedButton = filter.controllers[i].getElementsByClassName('js-filter-selected');
if(selectedButton.length > 0) {
var sort = selectedButton[0].getAttribute('data-sort');
sort
? setSortingString(filter, selectedButton[0].getAttribute('data-sort'), selectedButton[0])
: setFilterString(filter, i, 'button');
continue;
}
// input list
var selectedInput = filter.controllers[i].querySelectorAll('input:checked');
if(selectedInput.length > 0) {
var sort = selectedInput[0].getAttribute('data-sort');
sort
? setSortingString(filter, sort, selectedInput[0])
: setFilterString(filter, i, 'input');
continue;
}
// select item
if(filter.controllers[i].tagName.toLowerCase() == 'select') {
var sort = filter.controllers[i].getAttribute('data-sort');
sort
? setSortingString(filter, filter.controllers[i].value, filter.controllers[i].options[filter.controllers[i].selectedIndex])
: setFilterString(filter, i, 'select');
continue;
}
// check if there's a generic custom input
var radioInput = filter.controllers[i].querySelector('input[type="radio"]'),
checkboxInput = filter.controllers[i].querySelector('input[type="checkbox"]');
if(!radioInput && !checkboxInput) {
var sort = filter.controllers[i].getAttribute('data-sort');
var filterString = filter.controllers[i].getAttribute('data-filter');
if(sort) setSortingString(filter, sort, filter.controllers[i]);
else if(filterString) setFilterString(filter, i, 'custom');
}
}
updateFilterArray(filter);
};
function setSortingString(filter, value, item) {
// get sorting string value-> sortName:order:type
var order = item.getAttribute('data-sort-order') ? 'desc' : 'asc';
var type = item.getAttribute('data-sort-number') ? 'number' : 'string';
filter.sortingString = value+':'+order+':'+type;
};
function setFilterString(filter, index, type) {
// get filtering array -> [filter1:filter2, filter3, filter4:filter5]
if(type == 'input') {
var checkedInputs = filter.controllers[index].querySelectorAll('input:checked');
filter.filterString[index] = '';
for(var i = 0; i < checkedInputs.length; i++) {
filter.filterString[index] = filter.filterString[index] + checkedInputs[i].getAttribute('data-filter') + ':';
}
} else if(type == 'select') {
if(filter.controllers[index].multiple) { // select with multiple options
filter.filterString[index] = getMultipleSelectValues(filter.controllers[index]);
} else { // select with single option
filter.filterString[index] = filter.controllers[index].value;
}
} else if(type == 'button') {
var selectedButtons = filter.controllers[index].querySelectorAll('.js-filter-selected');
filter.filterString[index] = '';
for(var i = 0; i < selectedButtons.length; i++) {
filter.filterString[index] = filter.filterString[index] + selectedButtons[i].getAttribute('data-filter') + ':';
}
} else if(type == 'custom') {
filter.filterString[index] = filter.controllers[index].getAttribute('data-filter');
}
};
function resetControllersList(filter, index, target1, target2) {
// for a