Skip to content

Instantly share code, notes, and snippets.

@arielsalminen
Last active August 29, 2015 14:08
Show Gist options
  • Save arielsalminen/2aac0d7eae762c174352 to your computer and use it in GitHub Desktop.
Save arielsalminen/2aac0d7eae762c174352 to your computer and use it in GitHub Desktop.
//= require "ranking"
/*!
* Upstatic AutoSuggest v1.0
* http://upstatic.io
*/
(function (window, document, undefined) {
"use strict";
/**
* The AutoSuggest object
*
* @param {Element} The input element
* @param {Element} The list element
* @param {Element} The loading indicator element
* @param {integer} The breakpoint for the scroll-to-input functionality
* @constructor
*/
function AutoSuggest(elem, list, loader, breakpoint) {
this.field = document.querySelector(elem) || document.getElementById("search");
this.list = document.querySelector(list) || document.getElementById("suggestions");
this.loader = document.querySelector(loader) || document.querySelector(".loading");
this.links = this.list.getElementsByTagName("a");
this.breakpoint = breakpoint || 660;
this.placeholderText = "Type a keyword (eg. “caching”)";
this.placeholderTextMobile = "Type a keyword";
this.activeClass = "active";
this.visibleClass = "visible";
this.focusClass = "focus";
this.ticking = false;
}
AutoSuggest.prototype = {
constructor : AutoSuggest,
/**
* Intializes the instance
*
* @function
*/
init : function () {
var self = this;
if (util.isSupported && this.list && this.list !== "null") {
util.addListener(window, "blur", this, false);
util.addListener(window, "resize", this, false);
util.addListener(window, "keydown", this, false);
util.addListener(window, "orientationchange", this, false);
util.addListener(this.field, "focus", function () {
self.field.classList.add(self.focusClass);
}, false);
util.forEach(this.links, function (i) {
self.links[i].setAttribute("tabindex", "-1");
});
this.resize();
this.setupCache();
this.placeholderPolyfill();
this.toggleResults();
this.handleInputEvents();
this.handleMouseEvents();
}
},
/**
* Remove focus from search results
*
* @param {Event} event
* @function
*/
removeFocus : function (e) {
var self = this;
if (this.field === document.activeElement) return;
var focused = document.querySelectorAll("." + this.focusClass);
if (focused[0]) {
util.forEach(focused, function (i) {
focused[i].classList.remove(self.focusClass);
});
}
},
/**
* Takes care of basic event handling
*
* @param {event} event
* @return {type} returns the type of event that should be used
*/
handleEvent : function (e) {
var evt = e || window.event;
switch (evt.type) {
case "click":
this.clickOutside(evt);
break;
case "keydown":
this.removeFocus(evt);
break;
case "keyup":
this.toggleResultsOnKeyup(evt);
break;
case "orientationchange":
case "resize":
this.resize(evt);
break;
case "focus":
this.scrollIntoView(this.field);
break;
case "blur":
this.blur(evt);
break;
}
},
/**
* Stuff for window.resize and orientationchange
*
* @function
*/
resize : function () {
if (window.innerWidth < this.breakpoint) {
util.addListener(this.field, "focus", this, false);
this.field.setAttribute("placeholder", this.placeholderTextMobile);
} else {
util.removeListener(this.field, "focus", this, false);
this.field.setAttribute("placeholder", this.placeholderText);
}
},
/**
* Find element's real position
*
* @param {Element} element
* @function
*/
findPosition : function (elem) {
var top = 0;
if (elem.offsetParent) {
do {
top += elem.offsetTop;
} while (elem = elem.offsetParent);
return top;
}
},
/**
* Get current scroll position
*
* @function
*/
getCurrentPosition : function () {
// Firefox, Chrome, Opera, Safari
if (window.pageYOffset) return window.pageYOffset;
return 0;
},
/**
* Scroll element into viewport
*
* @param {Element} element
* @function
*/
scrollIntoView : function (elem) {
var startY = this.getCurrentPosition();
var stopY = this.findPosition(elem) - 30;
var distance = stopY > startY ? stopY - startY : startY - stopY;
var speed = Math.round(distance / 50);
if (speed >= 20) speed = 20;
var step = Math.round(distance / 25);
var leapY = stopY > startY ? startY + step : startY - step;
var timer = 0;
if (stopY > startY) {
for (var i = startY; i < stopY; i += step) {
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed);
leapY += step; if (leapY > stopY) leapY = stopY; timer++;
} return;
}
for (var i = startY; i > stopY; i -= step) {
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed);
leapY -= step; if (leapY < stopY) leapY = stopY; timer++;
}
},
/**
* Polyfill input placeholders
*
* @function
*/
placeholderPolyfill : function () {
var self = this;
if (!util.supportsPlaceholder) {
this.field.value = this.placeholderText;
util.addListener(this.field, "focus", function () {
if (this.value === self.placeholderText) this.value = "";
}, false);
util.addListener(this.field, "blur", function () {
if (this.value === "") this.value = self.placeholderText;
}, false);
}
},
/**
* Placeholder fallback
*
* @function
*/
placeholderFill : function () {
if (!util.supportsPlaceholder) {
this.field.value = this.placeholderText;
} else {
this.field.value = "";
}
},
/**
* Handle clicks outside of certain element
*
* @param {Event} event
* @function
*/
clickOutside : function (e) {
var level = 0;
for (var el = e.target; el; el = el.parentNode) {
if (el.id === this.list.id || el.id === this.field.id) {
return;
}
level++;
}
this.blur();
this.removeFocus();
},
/**
* Toggles results open & closed on keyup
*
* @param {Event} event
* @function
*/
toggleResultsOnKeyup : function (e) {
if (e.keyCode === 27) { // Esc
e.preventDefault();
this.blur();
this.removeFocus();
return;
}
if (history.pushState) {
history.pushState("", document.title, window.location.pathname);
}
if (this.field.value === "") return;
if (this.value !== "") {
if (window.getComputedStyle(this.list, null).getPropertyValue("display") === "none") {
this.list.style.display = "block";
this.list.scrollTop = 0;
}
} else {
this.list.style.display = "none";
}
},
/**
* Logic for toggling results
*
* @function
*/
toggleResults : function () {
var self = this;
util.addListener(document, "click", this, false);
util.addListener(this.field, "keyup", this, false);
util.addListener(this.field, "keydown", function (e) {
var evt = e || window.event;
// Space & Enter
if (evt.keyCode === 32 || evt.keyCode === 13) {
e.preventDefault(); return;
}
// Tab
if (e.keyCode === 9) {
self.list.style.display = "none";
self.removeFocus();
return;
}
}, false);
},
/**
* Handle submit
*
* @param {Event} event
* @param {Element} the active element
* @function
*/
handleSubmit : function (event, activeResult) {
var self = this;
event.preventDefault();
this.stopGettingScores();
if (activeResult) {
activeResult.classList.add("submit");
setTimeout(function () {
var href = activeResult.querySelector("a").getAttribute("href");
var id = href.replace("#", "");
var elem = document.getElementById(id);
window.scrollTo(0, self.findPosition(elem) - 40);
self.field.classList.remove(self.focusClass);
elem.classList.add(self.focusClass);
self.blur();
}, 150);
} else {
setTimeout(function () {
self.loader.classList.remove(self.activeClass);
}, 150);
}
},
/**
* Browse results up and down
*
* @param {Element} the active element
* @param {Element} the next element from active
* @param {Boolean} browser from the beginning, true or false
* @function
*/
browseResults : function (activeEl, next, fromBeginning) {
var el;
if (!activeEl) return;
// Determine which result to select next
next ? el = activeEl.nextElementSibling : el = activeEl.previousElementSibling;
while (el && !el.classList.contains(this.visibleClass)) {
next ? el = el.nextElementSibling : el = el.previousElementSibling;
}
// If element exists and it has a visible class
if (el && el.classList.contains(this.visibleClass)) {
activeEl.classList.remove(this.activeClass);
el.classList.add(this.activeClass);
activeEl = el;
// Scroll the active element into view
var topPos = 0;
if (!fromBeginning) {
topPos = activeEl.offsetTop || 0;
}
if (next) {
if (!fromBeginning) {
if (topPos > 180) this.list.scrollTop = topPos - 50;
} else {
this.list.scrollTop = topPos - 50;
}
} else {
this.list.scrollTop = topPos - 50;
}
}
},
/**
* Handles keyup & blur events
*
* @function
*/
handleInputEvents : function () {
var self = this;
util.addListener(this.field, "blur", function (e) {
self.stopGettingScores();
});
util.addListener(this.field, "keyup", function (e) {
self.ticking = false;
var evt = e || window.event;
// Prevent some keys like arrow left and right triggering result lookup
if (evt.keyCode === 16 || evt.keyCode === 18 || evt.keyCode === 37 || evt.keyCode === 39) {
e.preventDefault(); return;
}
var activeResult = self.list.querySelector("li." + self.activeClass);
if (evt.keyCode === 13) self.handleSubmit(e, activeResult); // Enter
// Arrow up and down
if (evt.keyCode === 40 || evt.keyCode === 38) {
e.preventDefault();
self.stopGettingScores();
if (activeResult) {
evt.keyCode === 40 ? self.browseResults(activeResult, true) : self.browseResults(activeResult, false);
// If no elements are active
} else {
if (evt.keyCode === 40) {
var childNodes = self.list.querySelectorAll("li." + self.visibleClass);
var first = util.firstOfType(childNodes, self.activeClass);
self.browseResults(self.list.querySelector("li." + self.activeClass), true, true);
}
}
return;
}
self.filter();
});
},
/**
* Handles mouse events
*
* @function
*/
handleMouseEvents : function () {
var savedState = this.list.querySelector("li." + this.activeClass);
var self = this;
util.forEach(this.links, function (i) {
// Select on mouseover
util.addListener(self.links[i], "mouseover", function () {
self.stopGettingScores();
savedState.classList.remove(self.activeClass);
self.links[i].parentNode.classList.add(self.activeClass);
savedState = self.links[i].parentNode;
}, false);
// Deselect on mouseout
util.addListener(self.links[i], "mouseout", function () {
self.links[i].parentNode.classList.remove(self.activeClass);
self.ticking = false;
}, false);
// Handle submit on click
util.addListener(self.links[i], "click", function (e) {
self.handleSubmit(e, self.links[i].parentNode);
}, false);
});
},
/**
* Filter results
*
* @function
*/
filter : function () {
var self = this;
if (this.field.value === "") {
util.forEach(this.list.children, function (i) {
self.list.children[i].style.display = "block";
});
return;
}
if (this.field === document.activeElement) { // If has :focus
this.loader.classList.add(this.activeClass);
}
if (!this.ticking) {
this.stopGettingScores();
self.timer = window.setInterval(function () {
self.displayLate();
}, 200);
}
},
/**
* Display late
*
* @function
*/
displayLate : function () {
this.displayResults(this.getScores(this.field.value.toLowerCase()));
this.loader.classList.remove(this.activeClass);
this.ticking = false;
},
/**
* Caching
*
* @function
*/
setupCache : function () {
var self = this;
this.cache = [];
this.rows = [];
util.forEach(this.list.children, function (i) {
self.cache.push(self.list.children[i].innerHTML.toLowerCase());
self.rows.push(self.list.children[i]);
});
this.cache_length = this.cache.length;
},
/**
* Display Results
*
* @param {string} string
* @function
*/
displayResults : function (scores) {
var self = this;
var firstRow = false;
util.forEach(this.list.children, function (i) {
self.list.children[i].style.display = "none";
self.list.children[i].className = "";
});
util.forEach(scores, function (i, score) {
self.rows[score[1]].style.display = "block";
self.rows[score[1]].classList.add(self.visibleClass);
});
var childNodes = this.list.querySelectorAll("li." + this.visibleClass);
util.firstOfType(childNodes, this.activeClass);
},
/**
* Get scores
*
* @param {string} string
* @function
*/
getScores : function (term) {
var scores = [];
for (var i=0; i < this.cache_length; i++) {
var score = this.cache[i].score(term);
if (score > 0) {
scores.push([score, i]);
}
}
return scores.sort(function (a, b) {
return b[0] - a[0];
});
},
/**
* Stop getting scores
*
* @function
*/
stopGettingScores : function () {
clearInterval(this.timer);
this.ticking = true;
},
/**
* Blur field
*
* @function
*/
blur : function () {
this.list.style.display = "none";
this.field.blur();
this.placeholderFill();
this.loader.classList.remove(this.activeClass);
}
}
/**
* Expose a public-facing API
*/
function expose(elem, list, loader, breakpoint) {
var autoSuggest = new AutoSuggest(elem, list, loader, breakpoint);
autoSuggest.init();
return autoSuggest;
}
window.autoSuggest = expose;
}(window, document));
// Init Upstatic AutoSuggest
autoSuggest();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment