Skip to content

Instantly share code, notes, and snippets.

@aarongeorge
Last active April 20, 2017 05:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aarongeorge/a0709594307738733f3b54b342ccdcfd to your computer and use it in GitHub Desktop.
Save aarongeorge/a0709594307738733f3b54b342ccdcfd to your computer and use it in GitHub Desktop.
/**
* Modules: FauxSelect
*
* Usage:
*
* new FauxSelect({
* 'classes': {
* 'active': '-active',
* 'fauxSelect': 'faux-select',
* 'selected': '-selected',
* 'wrapper': 'faux-select-wrapper'
* },
* 'contain': false,
* 'focusThreshold': 300,
* 'hideSelect': true,
* 'selectEl': document.querySelector('select'),
* 'transitionTime': '0.3s'
* });
*
*/
// Constructor: Faux Select
var FauxSelect = function (options) {
'use strict';
// Defaults
this.classes = {
'active': '-active',
'fauxSelect': 'faux-select',
'selected': '-selected',
'wrapper': 'faux-select-wrapper'
};
this.contain = false;
this.focusThreshold = 300;
this.hideSelect = true;
this.maxHeight = 'auto';
this.transitionTime = '0.3s';
// Set options
options = typeof options === 'undefined' ? {} : options;
// Override defaults
if (Object.keys(options).length) {
for (var i in options) {
if (options.hasOwnProperty(i)) {
this[i] = options[i];
}
}
}
// Call `init`
this.init();
};
// Method: init
FauxSelect.prototype.init = function () {
'use strict';
// Call `wrapSelectEl`
this.wrapSelectEl();
// Call `generateFauxHTML`
this.generateFauxHTML(this.selectEl);
// Call `appendFauxHTML`
this.appendFauxHTML(this.selectElWrapper);
// `hideSelect` is set
if (this.hideSelect) {
// Call `hideSelectEl`
this.hideSelectEl();
}
// Set data height attribute
this.updateDataHeight();
// Call `updateTransitionTime`
this.updateTransitionTime(this.transitionTime);
// Call `close`
this.close();
// Call `bindEvents`
this.bindEvents();
};
// Method: Generate Faux HTML
FauxSelect.prototype.generateFauxHTML = function (selectEl) {
// Array to store options text and value
this.optionsArr = [];
// Iterate over `selectEl.options`
for (var i = 0; i < selectEl.options.length; i++) {
// Add `text` and `value` to `optionsArr`
this.optionsArr[i] = {
'text': selectEl.options[i].text,
'value': selectEl.options[i].value,
'el': selectEl.options[i],
};
}
// Object to store elements
this.fauxSelectElements = {
'dl': document.createElement('DL'),
'dt': document.createElement('DT'),
'dd': document.createElement('DD'),
'ul': document.createElement('UL')
};
// Apply attributes, classes and styles
this.fauxSelectElements.dl.classList.add(this.classes.fauxSelect);
this.fauxSelectElements.dl.setAttribute('tabindex', 0);
this.fauxSelectElements.dd.style.overflow = 'hidden';
// Iterate over `optionsArr`
for (var j = 0; j < this.optionsArr.length; j++) {
// Create element
var liEl = document.createElement('LI');
// Set data-value
liEl.setAttribute('data-value', this.optionsArr[j].value);
// Set data-index
liEl.setAttribute('data-index', j);
// Set innerText
liEl.innerText = this.optionsArr[j].text;
// Append to `ul`
this.fauxSelectElements.ul.appendChild(liEl);
// Add `li` to `optionsArr`
this.optionsArr[j].fauxEl = liEl;
}
// Call `setSelectedOption`
this.setSelectedOption();
// Call `setActiveOption`
this.setActiveOption();
// Append children
this.fauxSelectElements.dl.appendChild(this.fauxSelectElements.dt);
this.fauxSelectElements.dl.appendChild(this.fauxSelectElements.dd);
this.fauxSelectElements.dd.appendChild(this.fauxSelectElements.ul);
// Return `dl`
return this.fauxSelectElements.dl;
};
// Method: appendFauxHTML
FauxSelect.prototype.appendFauxHTML = function (target) {
'use strict';
target.parentNode.insertBefore(this.fauxSelectElements.dl, target.nextSibling);
};
// Method: hideSelectEl
FauxSelect.prototype.hideSelectEl = function () {
'use strict';
this.selectEl.style.display = 'none';
};
// Method: showSelectEl
FauxSelect.prototype.showSelectEl = function () {
'use strict';
this.selectEl.style.display = 'inline';
};
// Method: getListHeight
FauxSelect.prototype.getListHeight = function () {
'use strict';
return this.fauxSelectElements.dd.offsetHeight;
};
// Method: updateDataHeight
FauxSelect.prototype.updateDataHeight = function () {
'use strict';
this.fauxSelectElements.dl.setAttribute('data-height', this.getListHeight());
};
// Method: updateTransitionTime
FauxSelect.prototype.updateTransitionTime = function (time) {
'use strict';
this.fauxSelectElements.dd.style.transition = 'height ' + time;
};
// Method: toggle
FauxSelect.prototype.toggle = function () {
'use strict';
// Switch over `state`
switch (this.state) {
// Open
case 'open': {
// Call `close`
this.close();
break;
}
// Closed
case 'closed': {
// Call `open`
this.open();
break;
}
}
};
// Method: setMaxHeight
FauxSelect.prototype.setMaxHeight = function () {
'use strict';
if (this.contain) {
var dtBounds = this.fauxSelectElements.dt.getBoundingClientRect();
this.fauxSelectElements.dd.style.maxHeight = dtBounds.bottom - dtBounds.height + 'px';
}
};
// Method: close
FauxSelect.prototype.close = function () {
'use strict';
// Set `state`
this.state = 'closed';
// Set `overflow` to `none`
this.fauxSelectElements.dd.style.overflow = 'hidden';
// Set `height` to `0`
this.fauxSelectElements.dd.style.height = '0px';
};
// Method: open
FauxSelect.prototype.open = function () {
'use strict';
// Set `state`
this.state = 'open';
// Call `setMaxHeight`
this.setMaxHeight();
// Set `overflow` to `auto`
this.fauxSelectElements.dd.style.overflow = 'auto';
// Set `height` to `data-height`
this.fauxSelectElements.dd.style.height = this.fauxSelectElements.dl.getAttribute('data-height') + 'px';
};
// Method: getSelectedOption
FauxSelect.prototype.getSelectedOption = function () {
'use strict';
// Store `selectedEl` as the first item in `optionsArr`
var selectedEl = this.optionsArr[0];
// Iterate over `optionsArr`
for (var j = 0; j < this.optionsArr.length; j++) {
// Check if current item has the `selected` attribute
if (this.optionsArr[j].el.hasAttribute('selected')) {
// Set `selectedEl` to current item
selectedEl = this.optionsArr[j];
}
}
// Return `selectedEl`
return selectedEl;
};
// Method: setSelectedOption
FauxSelect.prototype.setSelectedOption = function (i) {
'use strict';
// `selectedOption` hasn't been set yet
if (this.selectedOption === undefined) {
// Call `getSelectedOption` and save it
this.selectedOption = this.getSelectedOption();
}
// `selectedEl` has been set
else {
// Call `unsetSelectedOption`
this.unsetSelectedOption();
// Set `selectedOption`
this.selectedOption = this.optionsArr[i];
}
// Update `selectEl` value
this.selectEl.value = this.selectedOption.value;
// Set `innerText`
this.fauxSelectElements.dt.innerText = this.selectedOption.text;
// Add `selected` class
this.selectedOption.fauxEl.classList.add(this.classes.selected);
};
// Method: unsetSelectedOption
FauxSelect.prototype.unsetSelectedOption = function () {
'use strict';
// `selectedOption` hasn't been set yet
if (this.selectedOption === undefined) {
// Get outta here
return;
}
// `selectedOption` has been set
else {
this.selectedOption.fauxEl.classList.remove(this.classes.selected);
this.selectedOption = undefined;
this.fauxSelectElements.dt.innerText = this.selectedOption;
}
};
// Method: getActiveOption
FauxSelect.prototype.getActiveOption = function () {
'use strict';
// `selectedOption` is set
if (this.selectedOption !== undefined) {
// Return `selectedOption`
return this.selectedOption;
}
// `selectedOption` is not set
else {
// Store `activeEl` as the first item in `optionsArr`
var activeEl = this.optionsArr[0];
// Iterate over `optionsArr`
for (var j = 0; j < this.optionsArr.length; j++) {
// Check if current item has the `selected` class
if (this.optionsArr[j].el.classList.contains(this.classes.selected)) {
// Set `activeEl` to current item
activeEl = this.optionsArr[j];
}
}
// Return `activeEl`
return activeEl;
}
};
// Method: setActiveOption
FauxSelect.prototype.setActiveOption = function (i) {
'use strict';
// `activeOption` hasn't been set yet
if (this.activeOption === undefined) {
// Call `getActiveOption` and save it
this.activeOption = this.getActiveOption();
}
// `activeOption` has been set
else {
// Call `unsetActiveOption`
this.unsetActiveOption();
// Set `activeOption`
this.activeOption = this.optionsArr[i];
}
// Add `active` class
this.activeOption.fauxEl.classList.add(this.classes.active);
};
// Method: unsetActiveOption
FauxSelect.prototype.unsetActiveOption = function () {
'use strict';
// `activeOption` hasn't been set yet
if (this.activeOption === undefined) {
// Get outta here
return;
}
// `activeOption` has been set
else {
this.activeOption.fauxEl.classList.remove(this.classes.active);
this.activeOption = undefined;
}
};
// Method: bindEvents
FauxSelect.prototype.bindEvents = function () {
'use strict';
// Store reference to `this`
var _this = this;
// Change
this.selectEl.addEventListener('change', function () {
// Iterate over `optionsArr`
for (var i = 0; i < _this.optionsArr.length; i++) {
// Selected option matches one of the options in the array
if (_this.optionsArr[i].el === _this.selectEl.selectedOptions[0]) {
// Call `setSelectedOption`
_this.setSelectedOption(i);
// Call `setActiveOption`
_this.setActiveOption(i);
break;
}
}
});
// Focus
this.fauxSelectElements.dl.addEventListener('focus', function (e) {
// Update `lastFocus`
_this.lastFocus = window.performance.now();
// Set `hasFocus`
_this.hasFocus = true;
// Call `toggle`
_this.toggle();
});
// Blur
this.fauxSelectElements.dl.addEventListener('blur', function (e) {
// Call `close`
_this.close();
// Set `hasFocus`
_this.hasFocus = false;
});
// Click
this.fauxSelectElements.dl.addEventListener('click', function (e) {
// Clicked `dt`
if (e.target === _this.fauxSelectElements.dl.querySelector('dt')) {
// Click event happened less than `focusThreshold` after a focus event
if (window.performance.now() - _this.lastFocus < _this.focusThreshold) {
// Get out of here
return;
}
// Call `toggle`
_this.toggle();
}
// Clicked `li`
else if (e.target.nodeName === 'LI') {
// Call `setSelectedOption`
_this.setSelectedOption(e.target.getAttribute('data-index'));
// Call `setActiveOption`
_this.setActiveOption(e.target.getAttribute('data-index'));
// Call `close`
_this.close();
}
});
// Key Down
this.fauxSelectElements.dl.addEventListener('keydown', function (e) {
// Switch on `keyCode`
switch (e.keyCode) {
// Down Arrow
case 40: {
e.preventDefault();
break;
}
// Up Arrow
case 38: {
e.preventDefault();
break;
}
}
});
// Key Up
this.fauxSelectElements.dl.addEventListener('keyup', function (e) {
// Click event happened less than `focusThreshold` after a focus event
if (window.performance.now() - _this.lastFocus < _this.focusThreshold) {
// Get out of here
return;
}
// Store reference to selected index
var selectedIndex = _this.optionsArr.indexOf(_this.selectedOption);
var activeIndex = _this.optionsArr.indexOf(_this.activeOption);
// Switch on `keyCode`
switch (e.keyCode) {
// Enter
case 13: {
// Call `setActiveOption`
_this.setActiveOption(activeIndex);
// Call `setSelectedOption`
_this.setSelectedOption(activeIndex);
// Call `close`
_this.close();
break;
}
// Down Arrow
case 40: {
// Open if closed
if (_this.state === 'closed') {
_this.open();
}
// `activeIndex` is not the last item in `optionsArr`
if (activeIndex !== _this.optionsArr.length - 1) {
// Increment `activeIndex` and set it
_this.setActiveOption(activeIndex + 1);
}
break;
}
// Up Arrow
case 38: {
// Open if closed
if (_this.state === 'closed') {
_this.open();
}
// `activeIndex` is not the first item in `optionsArr`
if (activeIndex !== 0) {
// Decrement `activeIndex` and set it
_this.setActiveOption(activeIndex - 1);
}
break;
}
}
});
// Mouse move
this.fauxSelectElements.ul.addEventListener('mousemove', function (e) {
// Call `setActiveOption`
_this.setActiveOption(e.target.getAttribute('data-index'));
});
};
// Method: wrapSelectEl
FauxSelect.prototype.wrapSelectEl = function () {
'use strict';
var wrapEl = document.createElement('div');
wrapEl.classList.add(this.classes.wrapper);
if (this.selectEl.nextSibling) {
this.selectEl.parentNode.insertBefore(wrapEl, this.selectEl.nextSibling);
}
else {
this.selectEl.parentNode.appendChild(wrapEl);
}
this.selectElWrapper = wrapEl.appendChild(this.selectEl);
};
// Export `fauxSelect`
module.exports = FauxSelect;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment