// 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