Last active
April 20, 2017 05:13
-
-
Save aarongeorge/a0709594307738733f3b54b342ccdcfd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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