var app = {
ready (callback) = {
In case the document is already rendered
if (document.readyState!='loading') callback();
else document.addEventListener('DOMContentLoaded', callback);
},
menu {},
search {},
keys {},
overlay {},
animations { tracked [] }
};
app.ready(() = {
app.search.init();
app.menu.init();
app.keys.init();
app.overlay.init();
Listen to keys, close menu if visible
document.addEventListener(keyup, (e) = { if (e.keyCode == app.keys.ESC) app.keys.handleESC() });
document.addEventListener(keydown, (e) = {
if (e.keyCode == app.keys.arrowUp) app.keys.handleArrowUp(e);
else if (e.keyCode == app.keys.arrowDown) app.keys.handleArrowDown(e);
else if (e.keyCode == app.keys.enter) app.keys.handleEnter(e);
});
window.addEventListener(scroll, app.animations.onlyPlayVisible);
});
app.search.init = () = {
Exit if there's no search function on the page
app.search.visible = false;
app.search.storageKey = globalSearchData;
var searchIcon = document.querySelector(.js-search);
if (!searchIcon) return;
app.search.searchIcon = searchIcon;
app.search.loadData();
React to menu events
document.addEventListener(appmenuDidHide, app.search.showIcon);
document.addEventListener(appmenuWillShow, app.search.hideIcon);
Click handlers
document.querySelector(.js-search).addEventListener(click, (e) = {
e.preventDefault();
!app.search.visible app.search.reveal(e) app.search.hide();
});
When searching
document.querySelector(.js-search-input).addEventListener(input, (e) = {
app.search.updateResultsForQuery(e.target.value);
});
}
app.search.loadData = () = {
Check if data already exists, if so load it instead
var cachedData = localStorage.getItem(app.search.storageKey);
if (cachedData) {
var data = JSON.parse(cachedData);
app.search.data = data[items];
return;
}
If not, cache this with local storage and don't fetch on every page load
fetch(jssearchable.json).then(response = {
return response.json();
}).then((data) = {
localStorage.setItem(app.search.storageKey, JSON.stringify(data));
app.search.data = data[items];
}).catch((err) = {
Handle error
});
}
app.search.updateResultsForQuery = (query) = {
query = query.toLowerCase();
var hits = [];
Look through all items
for (var i = 0; i app.search.data.length; i++) {
For every item, look for hits
var entryValues = Object.values(app.search.data[i]);
var searchString = entryValues.join( ).toLowerCase();
var hit = searchString.indexOf(query);
Store new hit
if (hit == -1) continue;
hits.push(app.search.data[i]);
}
app.search.renderResults(hits, query);
}
app.search.renderResults = (results, query) = {
var searchElements = document.createElement(div);
searchElements.classList.add(site-search-content-results-list);
for (var i = 0; i results.length; i++) {
Create link
var newResult = document.createElement(a);
Add active if first row
var rowClass = site-search-results-item js-site-search-results-item;
if (i == 0) rowClass += site-search-results-item-active;
newResult.classList = rowClass;
newResult.href = results[i][url];
newResult.textContent = results[i][title];
Create span and append to link
var linkSpan = document.createElement(span);
linkSpan.classList = site-search-results-item-description;
linkSpan.textContent = results[i][description];
newResult.appendChild(linkSpan);
Append row to results
searchElements.appendChild(newResult);
}
If length is 0, add a placeholder saying you found nothing
if (results.length == 0) {
var noResult = document.createElement(span);
noResult.classList = site-search-results-item site-search-results-item-message;
noResult.textContent = 'No hits for ' + query + '';
searchElements.appendChild(noResult);
}
Update HTML
var results = document.querySelector(.js-site-search-content-results);
results.innerHTML = ;
results.appendChild(searchElements);
Update mouseenter event to move focus to hovered item
for (let resultsItem of document.querySelectorAll(.js-site-search-results-item)) {
resultsItem.addEventListener(mouseenter, (e) = app.search.focusItem(e.target));
}
}
app.menu.init = () = {
app.menu.visible = false;
app.menu.icon = document.querySelector(.menu);
Top menu
app.menu.icon.addEventListener(click, (e) = {
e.preventDefault();
!app.menu.visible app.menu.reveal(e) app.menu.hide();
});
}
app.menu.toggleStates = () = {
document.querySelector('body').classList.toggle('no-scroll');
app.menu.icon.classList.toggle('menu-active');
document.querySelector('.js-nav').classList.toggle('site-nav-active');
}
app.search.toggleStates = () = {
document.querySelector('body').classList.toggle('no-scroll');
document.querySelector('.js-search-overlay').classList.toggle('site-nav-active');
}
app.menu.reveal = (e) = {
app.menu.visible = true;
app.menu.toggleStates();
document.dispatchEvent(new Event(appmenuWillShow));
app.overlay.show({
position app.clickPosition(e),
fill #1f4954
});
var containerDelay = 200;
anime({
targets'.js-nav',
opacity [0, 1],
delay containerDelay,
easing easeInOutExpo,
duration 200
});
var menuItemDelay = 90;
containerDelay += 75;
document.querySelector(.js-nav-header).style.opacity = 0;
anime({
targets .js-nav-header,
opacity [0,1],
delay containerDelay,
easing easeInOutExpo,
duration 200
});
document.querySelector(.js-nav-header-line).style.transform.replace(scale([0-9.]), 'scale(0.2)');
anime({
targets'.js-nav-header-line',
scaleX [0.28, 1],
delay containerDelay,
easing easeInOutExpo,
duration 600
});
containerDelay += 350;
for (let animated of document.querySelectorAll(.js-nav-animate)) {
animated.style.opacity = 0;
animated.style.transform.replace(scale([0-9.]), 'scale(0.9)');
}
anime({
targets '.js-nav-animate',
translateY [-7px, 0],
scale [0.9, 1],
opacity [0, 1],
delay (el, i) = containerDelay + menuItemDelay (i+1),
duration 1100,
easing easeOutExpo,
complete () = { document.dispatchEvent(new Event('appmenuDidReveal')) }
});
}
app.search.reveal = (e, ) = {
app.search.toggleStates();
app.search.visible = true;
app.menu.hideMenuIcon();
app.overlay.show({
position app.clickPosition(e),
fill #1f4954
});
Hide search icon and show X
var searchIconDuration = 400;
var searchIconDelay = 200;
Hide Search icon
anime({
targets '.site-search-icon',
translateY -5px,
rotate 90,
duration searchIconDuration,
scale 0,
easing 'easeOutExpo',
delay searchIconDelay
});
Show close icon
anime({
targets '.site-search-close-icon',
opacity 1,
scale [0,1],
duration searchIconDuration,
easing 'easeOutExpo',
delay searchIconDelay
});
anime({
targets '.site-search-close-icon-line-1',
rotateZ [45, 225],
duration searchIconDuration,
easing 'easeOutExpo',
delay searchIconDelay
});
anime({
targets '.site-search-close-icon-line-2',
rotateZ [45, 135],
duration searchIconDuration,
easing 'easeOutExpo',
delay searchIconDelay
});
document.querySelector(.js-search-input).style.opacity = 0;
anime.timeline().add({
targets'.js-search-overlay',
opacity [0, 1],
delay 200,
easing easeInOutExpo,
duration 200
}).add({
targets '.js-search-input',
opacity [0,1],
easing easeOutExpo,
translateX [25px, 0],
duration 700
});
Focus on input field
document.querySelector(.js-search-input).focus();
}
app.search.moveSelectionInDirection = (options) = {
Find index of current focus
var activeSelection = document.querySelector(.site-search-results-item-active);
if (!activeSelection) return;
var newSelection = options.direction === up activeSelection.previousElementSibling activeSelection.nextElementSibling;
Select next item (if any)
if (newSelection == null) return;
activeSelection.classList.remove(site-search-results-item-active);
newSelection.classList.add(site-search-results-item-active);
}
app.search.moveSelectionUp = () = app.search.moveSelectionInDirection({direction up});
app.search.moveSelectionDown = () = app.search.moveSelectionInDirection({direction down});
app.search.focusItem = (item) = {
document.querySelector(.site-search-results-item-active).classList.remove(site-search-results-item-active);
item.classList.add(site-search-results-item-active);
}
app.search.goToSelectedItem = () = {
var activeItem = document.querySelector(.site-search-results-item-active);
if (!activeItem) return;
window.location.href = activeItem.getAttribute(href);
}
app.search.hide = () = {
app.search.toggleStates();
app.search.visible = false;
var searchIconDuration = 400;
var searchIconDelay = 200;
app.overlay.hide({
position app.overlay.lastStartingPoint,
fill #1f4954,
complete () = app.menu.showMenuIcon()
});
anime({
targets '.js-search-input',
opacity [1,0],
easing easeInExpo,
duration 400,
translateX [0, 25px]
});
anime({
targets'.js-search-overlay',
opacity [1, 0],
delay 200,
easing easeInOutExpo,
duration 200
});
Animate the cross
anime({
targets '.site-search-close-icon',
opacity [1,0],
scale 0,
duration searchIconDuration,
easing 'easeOutExpo',
delay searchIconDelay
});
anime({
targets '.site-search-close-icon-line-1',
rotateZ [225, 45],
duration searchIconDuration,
easing 'easeOutExpo',
delay searchIconDelay
});
anime({
targets '.site-search-close-icon-line-2',
rotateZ [135, 45],
duration searchIconDuration,
easing 'easeOutExpo',
delay searchIconDelay
});
anime({
targets '.site-search-icon',
translateY [-5px, 0px],
rotate [90,0],
duration searchIconDuration,
opacity [0,1],
scale [0,1],
easing 'easeOutExpo',
delay searchIconDelay
});
}
app.menu.hide = () = {
app.menu.visible = false;
app.menu.toggleStates();
document.dispatchEvent(new Event(appmenuWillHide));
app.overlay.hide({
position app.overlay.lastStartingPoint,
fill #1f4954,
complete () = document.dispatchEvent(new Event(appmenuDidHide))
});
anime({
targets'.js-nav',
opacity [1, 0],
easing easeInOutExpo,
duration 200
});
anime({
targets'.js-nav-header-line',
scale [1, 0.5],
easing easeInExpo,
duration 300
});
anime({
targets '.js-nav-animate',
translateY 10px,
scale [1, 0.9],
opacity [1, 0],
easing easeInExpo,
duration 200
});
}
app.menu.hideMenuIcon = () = {
app.menu.icon.style.display = none;
}
app.menu.showMenuIcon = () = {
app.menu.icon.style.opacity = 0;
app.menu.icon.style.display = ;
anime({
targets '.menu',
opacity 1,
duration 500,
easing 'easeOutQuart'
});
}
app.search.hideIcon = () = {
if (!app.search.searchIcon) return;
app.search.searchIcon.style.display = none;
}
app.search.showIcon = () = {
if (!app.search.searchIcon) return;
app.search.searchIcon.style.opacity = 0;
app.search.searchIcon.style.display = ;
anime({
targets .js-search,
opacity 1,
duration 500,
easing 'easeOutQuart'
});
}
app.keys.handleESC = () = {
document.dispatchEvent(new Event(pressedESC));
if (app.menu.visible) {
app.menu.hide();
return;
}
if (app.search.visible) {
app.search.hide();
return;
}
}
Keyboard Key handling
app.keys.init = () = {
app.keys.ESC = 27;
app.keys.arrowUp = 38;
app.keys.arrowDown = 40;
app.keys.enter = 13;
}
app.keys.handleArrowUp = (e) = {
if (app.search.visible) {
e.preventDefault();
app.search.moveSelectionUp();
}
}
app.keys.handleArrowDown = (e) = {
if (app.search.visible) {
e.preventDefault();
app.search.moveSelectionDown();
}
}
app.keys.handleEnter = (e) = {
if (app.search.visible) {
e.preventDefault();
app.search.goToSelectedItem();
}
}
Management of animations
app.animations.track = (animeTimeline, el) = {
Add object to list of tracked animations
app.animations.tracked.push({
timeline animeTimeline,
element el
});
}
app.animations.onlyPlayVisible = () = {
app.animations.tracked.forEach((animation) = {
app.animations.shouldPlay(animation) animation.timeline.play() animation.timeline.pause();
});
}
app.animations.shouldPlay = (animation) = {
var winHeight = window.innerHeight;
var bounds = animation.element.getBoundingClientRect();
var offset = 5; Greater offset - animations will play less often
Check if bottom of animation is above view or if top of animation is below view
if (bounds.bottom 0+offset bounds.top winHeight-offset) return false;
Default to true
return true;
}
app.overlay.init = () = {
app.overlay.c = document.querySelector(.site-nav-canvas);
app.overlay.ctx = app.overlay.c.getContext(2d);
app.overlay.cH;
app.overlay.cW;
app.overlay.bgColor = transparent;
app.overlay.resizeCanvas();
app.overlay.lastStartingPoint = {x 0, y 0};
window.addEventListener(resize, app.overlay.resizeCanvas);
}
app.overlay.resizeCanvas = function() {
app.overlay.cW = window.innerWidth;
app.overlay.cH = window.innerHeight;
app.overlay.c.width = app.overlay.cW window.devicePixelRatio;
app.overlay.c.height = app.overlay.cH window.devicePixelRatio;
app.overlay.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
app.overlay.ctx.fillStyle = app.overlay.bgColor;
app.overlay.ctx.fillRect(0, 0, app.overlay.cW, app.overlay.cH);
}
app.overlay.show = function(options) {
app.overlay.c.style.display = block;
app.overlay.lastStartingPoint = options.position;
options.targetRadius = app.overlay.calcPageFillRadius(options.position.x, options.position.y);
options.startRadius = 0;
options.easing = easeOutQuart;
app.overlay.animate(options);
}
Hide the overlay. Args
fill color to animate with
position position to target as the circle shrinks
complete completion callback
app.overlay.hide = (options) = {
options.targetRadius = 0;
options.easing = easeInOutQuart;
var callback = options.complete;
options.complete = () = {
app.overlay.c.style.display = none;
app.overlay.bgColor = transparent;
if (callback) callback();
};
options.startRadius = app.overlay.calcPageFillRadius(options.position.x, options.position.y);
app.overlay.animate(options);
}
Animate from one size to another. Args
position {x, y}
fill color
startRadius number
targetRadius number
complete callback method
app.overlay.animate = (options) = {
var minCoverDuration = 750;
app.overlay.bgColor = options.fill;
app.overlay.circle.x = options.position.x;
app.overlay.circle.y = options.position.y;
app.overlay.circle.r = options.startRadius;
app.overlay.circle.fill = options.fill;
anime({
targets app.overlay.circle,
r options.targetRadius,
duration Math.max(options.targetRadius2, minCoverDuration),
easing options.easing,
complete options.complete options.complete null,
update () = app.overlay.circle.draw({
startRadius options.startRadius,
targetRadius options.targetRadius
})
});
}
app.overlay.calcPageFillRadius = function(x, y) {
var l = Math.max(x - 0, app.overlay.cW - x);
var h = Math.max(y - 0, app.overlay.cH - y);
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
}
app.clickPosition = (e) = {
if (e.touches) e = e.touches[0];
if (e.clientX && e.clientY) return {
x e.clientX,
y e.clientY
}
If there was no clientX and Y set, use the center position of
the target as a backup
var rect = e.target.getBoundingClientRect();
return {
x rect.top + (rect.bottom - rect.top)2,
y rect.left + (rect.right - rect.left)2
}
}
app.overlay.circle = {};
app.overlay.circle.draw = function(options) {
if (options.targetRadius options.startRadius) {
app.overlay.ctx.clearRect(0,0, app.overlay.cW, app.overlay.cH);
}
app.overlay.ctx.beginPath();
app.overlay.ctx.arc(this.x, this.y, this.r, 0, 2 Math.PI, false);
app.overlay.ctx.fillStyle = this.fill;
app.overlay.ctx.fill();
app.overlay.ctx.closePath();
}