flounder
Version:
a native friendly dropdown menu
1,607 lines (1,353 loc) • 41.4 kB
JavaScript
/*
* Copyright (c) 2015-2018 dunnhumby Germany GmbH.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the LICENSE file
* in the root directory of this source tree.
*
*/
/* globals console, document, setTimeout, window */
import utils from './utils';
import keycodes from './keycodes';
const events = {
/**
* ## addFirstTouchListeners
*
* adds the listeners for onFirstTouch
*
* @return {Void} void
*/
addFirstTouchListeners()
{
const refs = this.refs;
refs.selected.addEventListener( 'click', this.firstTouchController );
refs.select.addEventListener( 'focus', this.firstTouchController );
if ( this.props.openOnHover )
{
refs.wrapper.addEventListener( 'mouseenter',
this.firstTouchController );
}
},
/**
* ## addHoverClass
*
* adds a hover class to an element
*
* @param {Object} e event object
*
* @return {Void} void
*/
addHoverClass( e )
{
utils.addClass( e.target, this.classes.HOVER );
},
/**
* ## addListeners
*
* adds listeners on render
*
* @param {Object} refs DOM element references
*
* @return {Void} void
*/
addListeners( refs )
{
const ios = this.isIos;
const changeEvent = ios ? 'blur' : 'change';
refs.select.addEventListener( changeEvent, this.divertTarget );
refs.flounder.addEventListener( 'keydown', this.checkFlounderKeypress );
if ( this.props.openOnHover )
{
const wrapper = refs.wrapper;
wrapper.addEventListener( 'mouseenter', this.toggleList );
wrapper.addEventListener( 'mouseleave', this.toggleList );
}
else
{
refs.selected.addEventListener( 'click', this.toggleList );
}
this.addFirstTouchListeners();
this.addOptionsListeners();
if ( this.search )
{
this.addSearchListeners();
}
},
/**
* ## addMultipleTags
*
* adds a tag for each selected option and attaches the correct events to it
*
* @param {Array} selectedOptions currently selected options
* @param {DOMElement} multiTagWrapper parent element of the tags
*
* @return {Void} void
*/
addMultipleTags( selectedOptions, multiTagWrapper )
{
selectedOptions.forEach( option =>
{
if ( option.value !== '' )
{
const tag = this.buildMultiTag( option );
multiTagWrapper.insertBefore( tag, multiTagWrapper.lastChild );
}
else
{
option.selected = false;
}
} );
const children = multiTagWrapper.children;
for ( let i = 0; i < children.length - 1; i++ )
{
const tag = children[ i ];
const closeBtn = tag.firstChild;
closeBtn.addEventListener( 'click', this.removeMultiTag );
tag.addEventListener( 'keydown', this.checkMultiTagKeydown );
}
},
/**
* ## addOptionsListeners
*
* adds listeners to the options
*
* @return {Void} void
*/
addOptionsListeners()
{
this.refs.data.forEach( dataObj =>
{
if ( dataObj.tagName === 'DIV' )
{
dataObj.addEventListener( 'mouseenter', this.addHoverClass );
dataObj.addEventListener( 'mouseleave', this.removeHoverClass );
dataObj.addEventListener( 'click', this.clickSet );
}
} );
},
/**
* ## addNoMoreOptionsMessage
*
* Adding 'No More Options' message to the option list
*
* @return {Void} void
*/
addNoMoreOptionsMessage()
{
const classes = this.classes;
const elProps = {
className : classes.OPTION.push( classes.NO_RESULTS )
};
const noMoreOptionsEl = this.refs.noMoreOptionsEl ||
utils.constructElement( elProps );
noMoreOptionsEl.innerHTML = this.noMoreOptionsMessage;
this.refs.optionsList.appendChild( noMoreOptionsEl );
this.refs.noMoreOptionsEl = noMoreOptionsEl;
},
/**
* ## addNoResultsMessage
*
* Adding 'No Results' message to the option list
*
* @return {Void} void
*/
addNoResultsMessage()
{
const classes = this.classes;
const elProps = {
className : classes.OPTION.push( classes.NO_RESULTS )
};
const noResultsEl = this.refs.noResultsEl ||
utils.constructElement( elProps );
noResultsEl.innerHTML = this.noMoreResultsMessage;
this.refs.optionsList.appendChild( noResultsEl );
this.refs.noResultsEl = noResultsEl;
},
/**
* ## addPlaceholder
*
* determines what (if anything) should be refilled into the the
* placeholder position
*
* @return {Void} void
*/
addPlaceholder()
{
const selectedValues = this.getSelectedValues();
const val = selectedValues[ 0 ];
const selectedItems = this.getSelected();
const selectedText = selectedItems.length ?
selectedItems[ 0 ].innerHTML : '';
const selectedCount = selectedValues.length;
const selected = this.refs.selected;
switch ( selectedCount )
{
case 0:
this.setByIndex( 0 );
break;
case 1:
selected.innerHTML = selectedText;
break;
default:
selected.innerHTML = this.multipleMessage;
break;
}
if ( this.multipleTags )
{
if ( selectedCount === 0 )
{
this.setByIndex( 0 );
}
if ( !val || val === '' )
{
selected.innerHTML = this.placeholder;
}
else
{
selected.innerHTML = '';
}
}
},
/**
* ## addSearchListeners
*
* adds listeners to the search box
*
* @return {Void} void
*/
addSearchListeners()
{
const search = this.refs.search;
const multiTagWrapper = this.refs.multiTagWrapper;
this.debouncedFuzzySearch = utils.debounce( this.fuzzySearch, 200 );
if ( multiTagWrapper )
{
multiTagWrapper.addEventListener( 'click',
this.toggleListSearchClick );
}
search.addEventListener( 'click', this.toggleListSearchClick );
search.addEventListener( 'focus', this.toggleListSearchClick );
search.addEventListener( 'keyup', this.debouncedFuzzySearch );
search.addEventListener( 'focus', this.clearPlaceholder );
},
/**
* ## addSelectKeyListener
*
* adds a listener to the selectbox to allow for seeking through the native
* selectbox on keypress
*
* @return {Void} void
*/
addSelectKeyListener()
{
const select = this.refs.select;
select.addEventListener( 'keyup', this.setSelectValue );
select.addEventListener( 'keydown', this.setKeypress );
// weird shit
// http://stackoverflow.com/questions/34660500/mobile-safari-multi-select-bug
if ( this.isIos )
{
const classes = this.classes;
const firstOption = select.children[ 0 ];
const plug = document.createElement( 'OPTION' );
plug.disabled = true;
plug.setAttribute( 'disabled', true );
plug.className = classes.PLUG;
select.insertBefore( plug, firstOption );
}
},
/**
* ## catchBodyClick
*
* checks if a click is on the menu and, if it isnt, closes the menu
*
* @param {Object} e event object
*
* @return {Void} void
*/
catchBodyClick( e )
{
if ( !this.checkClickTarget( e ) )
{
this.toggleList( e );
this.addPlaceholder();
}
},
/**
* ## checkClickTarget
*
* checks whether the target of a click is the menu or not
*
* @param {Object} e event object
* @param {DOMElement} target click target
*
* @return {Boolean}click flouncer or not
*/
checkClickTarget( e, target )
{
target = target || e.target;
if ( target === document )
{
return false;
}
else if ( target === this.refs.flounder )
{
return true;
}
target = target.parentNode;
if ( target )
{
return this.checkClickTarget( e, target );
}
return false;
},
/**
* ## checkEnterOnSearch
*
* if enter is pressed in the searchox, if there is only one option
* matching, this selects it
*
* @param {Object} e event object
* @param {Object} refs element references
*
* @return {Void} void
*/
checkEnterOnSearch( e, refs )
{
const val = e.target.value;
if ( val && val !== '' )
{
const res = [];
const selected = this.getSelected();
const matches = this.search.isThereAnythingRelatedTo( val );
matches.forEach( el =>
{
const index = el.i;
el = refs.selectOptions[ index ];
if ( selected.indexOf( el ) === -1 )
{
res.push( el );
}
} );
// If only one result is available, select that result.
// If more than one results, select one only on exact match.
let selectedIndex = -1;
if ( res.length === 1 )
{
selectedIndex = 0;
}
else if ( res.length > 1 )
{
for ( let i = 0; i < res.length ; i++ )
{
if ( res[ i ].text.toUpperCase() === val.toUpperCase() )
{
selectedIndex = i;
break;
}
}
}
if ( selectedIndex != -1 )
{
const el = res[ selectedIndex ];
if ( !el.disabled )
{
this.setByIndex( el.index, this.multiple );
if ( this.multipleTags )
{
setTimeout( () => refs.search.focus(), 200 );
}
if ( this.onChange )
{
try
{
this.onChange( e, this.getSelectedValues() );
}
catch ( e )
{
console.warn(
'something may be wrong in "onChange"', e );
}
}
}
}
return res;
}
return false;
},
/**
* ## checkFlounderKeypress
*
* checks flounder focused keypresses and filters all but space and enter
*
* @param {Object} e event object
*
* @return {Void} void
*/
checkFlounderKeypress( e )
{
const keyCode = e.keyCode;
const refs = this.refs;
const classes = this.classes;
if ( keyCode === keycodes.TAB )
{
const optionsList = refs.optionsListWrapper;
const wrapper = refs.wrapper;
this.addPlaceholder();
this.toggleClosed( e, optionsList, refs, wrapper, true );
}
else if ( keyCode === keycodes.ENTER || keyCode === keycodes.SPACE )
{
if ( keyCode === keycodes.ENTER )
{
e.preventDefault();
e.stopPropagation();
if ( this.search &&
utils.hasClass( refs.wrapper, classes.OPEN ) )
{
return this.checkEnterOnSearch( e, refs );
}
}
if ( e.target.tagName !== 'INPUT' )
{
this.toggleList( e );
}
}
// letters - allows native behavior
else if ( keyCode >= 48 && keyCode <= 57 ||
keyCode >= 65 && keyCode <= 90 )
{
if ( refs.search && e.target.tagName === 'INPUT' )
{
refs.selected.innerHTML = '';
}
}
},
/**
* ## checkMultiTagKeydown
*
* when a tag is selected, this decides how to handle it by either passing
* the event on, or handling tag removal
*
* @param {Object} e event object
*
* @return {Void} void
*/
checkMultiTagKeydown( e )
{
const { keyCode, target } = e;
const catchKeys = [
keycodes.BACKSPACE,
keycodes.LEFT,
keycodes.RIGHT
];
if ( catchKeys.indexOf( keyCode ) !== -1 )
{
e.preventDefault();
e.stopPropagation();
if ( keyCode === keycodes.BACKSPACE )
{
this.checkMultiTagKeydownRemove( target );
}
else
{
this.checkMultiTagKeydownNavigate( keyCode, target );
}
}
else if ( e.key.length < 2 )
{
setTimeout( () => this.refs.search.focus(), 0 );
}
},
/**
* ## checkMultiTagKeydownNavigate
*
* after left or right is hit while a multitag is focused, this focuses on
* the next tag in that direction or the the search field
*
* @param {Number} keyCode keycode from the keypress event
* @param {DOMElement} target focused multitag
*
* @return {Void} void
*/
checkMultiTagKeydownNavigate( keyCode, target )
{
if ( keyCode === keycodes.LEFT )
{
const prev = target.previousSibling;
if ( prev )
{
prev.focus();
}
}
else if ( keyCode === keycodes.RIGHT )
{
const next = target.nextSibling;
if ( next )
{
setTimeout( () => next.focus(), 0 );
}
}
},
/**
* ## checkMultiTagKeydownRemove
*
* after a backspace while a multitag is focused, this removes the tag and
* focuses on the next
*
* @param {DOMElement} target focused multitag
*
* @return {Void} void
*/
checkMultiTagKeydownRemove( target )
{
const prev = target.previousSibling;
const next = target.nextSibling;
target.firstChild.click();
if ( prev )
{
setTimeout( () => prev.focus(), 0 );
}
else if ( next )
{
next.focus();
}
},
/**
* ## clearPlaceholder
*
* clears the placeholder
*
* @param {Object} e event object
*
* @return {Void} void
*/
clearPlaceholder()
{
this.refs.selected.innerHTML = '';
},
/**
* ## clickSet
*
* when a flounder option is clicked on it needs to set the option as
* selected
*
* @param {Object} e event object
*
* @return {Void} void
*/
clickSet( e )
{
e.preventDefault();
e.stopPropagation();
this.setSelectValue( {}, e );
if ( !this.programmaticClick )
{
this.toggleList( e, 'close' );
}
this.programmaticClick = false;
},
/**
* ## displayMultipleTags
*
* handles the display and management of tags
*
* @param {Array} selectedOptions currently selected options
* @param {DOMElement} multiTagWrapper div to display
* currently selected options
*
* @return {Void} void
*/
displayMultipleTags( selectedOptions, multiTagWrapper )
{
let tag = this.refs.search.previousSibling;
while ( tag )
{
const closeBtn = tag.firstChild;
const prevTag = tag.previousSibling;
closeBtn.removeEventListener( 'click', this.removeMultiTag );
tag.removeEventListener( 'keydown', this.checkMultiTagKeydown );
multiTagWrapper.removeChild( tag );
tag = prevTag;
}
if ( selectedOptions.length > 0 )
{
this.addMultipleTags( selectedOptions, multiTagWrapper );
}
else
{
this.addPlaceholder();
}
},
/**
* ## displaySelected
*
* formats and displays the chosen options
*
* @param {DOMElement} selected display area for the selected option(s)
* @param {Object} refs element references
*
* @return {Void} void
*/
displaySelected( selected, refs )
{
let value = [];
let index = -1;
const selectedOption = this.getSelected();
const selectedLength = selectedOption.length;
const multipleTags = this.multipleTags;
utils.addClass( selected, this.classes.SELECTED_DISPLAYED );
if ( !multipleTags && selectedLength === 1 )
{
index = selectedOption[ 0 ].index;
selected.innerHTML = refs.data[ index ].innerHTML;
value = selectedOption[ 0 ].value;
const extraClass = refs.data[ index ].extraClass;
if ( extraClass )
{
utils.addClass( selected, extraClass );
}
}
else if ( !multipleTags && selectedLength === 0 )
{
const defaultValue = this.defaultObj;
index = defaultValue.index || -1;
selected.innerHTML = defaultValue.text;
value = defaultValue.value;
}
else
{
if ( multipleTags )
{
selected.innerHTML = '';
this.displayMultipleTags( selectedOption,
refs.multiTagWrapper );
}
else
{
selected.innerHTML = this.multipleMessage;
}
index = selectedOption.map( option => option.index );
value = selectedOption.map( option => option.value );
}
selected.setAttribute( 'data-value', value );
selected.setAttribute( 'data-index', index );
},
/**
* ## divertTarget
*
* @param {Object} e event object
*
* on interaction with the raw select box, the target will be diverted to
* the corresponding flounder list element
*
* @return {Void} void
*/
divertTarget( e )
{
// weird shit
// http://stackoverflow.com/questions/34660500/mobile-safari-multi-select-bug
if ( this.isIos )
{
const select = this.refs.select;
const classes = this.classes;
const plug = select.querySelector( `.${classes.PLUG}` );
if ( plug )
{
select.removeChild( plug );
}
}
const index = e.target.selectedIndex;
const event = {
type : e.type,
target : this.data[ index ]
};
if ( this.multipleTags )
{
e.preventDefault();
e.stopPropagation();
}
this.setSelectValue( event );
if ( !this.multiple )
{
this.toggleList( e, 'close' );
}
},
/**
* ## firstTouchController
*
* on first interaction, onFirstTouch is run, then the event listener is
* removed
*
* @param {Object} e event object
*
* @return {Void} void
*/
firstTouchController( e )
{
const refs = this.refs;
if ( this.onFirstTouch )
{
try
{
this.onFirstTouch( e );
}
catch ( e )
{
console.warn( 'something may be wrong in "onFirstTouch"', e );
}
}
refs.selected.removeEventListener( 'click', this.firstTouchController );
refs.select.removeEventListener( 'focus', this.firstTouchController );
if ( this.props.openOnHover )
{
refs.wrapper.removeEventListener( 'mouseenter',
this.firstTouchController );
}
},
/**
* ## hideEmptySection
*
* Check if the provided element is indeed a section. If it is, check if
* it must to be shown or hidden.
*
* @param {DOMElement} se the section to be checked
*
* @return {Void} void
*/
hideEmptySection( se )
{
const selectedClass = this.selectedClass;
const sections = this.refs.sections;
for ( let i = 0; i < sections.length; ++i )
{
if ( sections[ i ] === se )
{
let shouldBeHidden = true;
// Ignore the title in childNodes[0]
for ( let j = 1; j < se.childNodes.length; j++ )
{
if ( !utils.hasClass( se.childNodes[ j ], selectedClass ) )
{
shouldBeHidden = false;
break;
}
}
if ( shouldBeHidden )
{
utils.addClass( se, selectedClass );
}
else
{
utils.removeClass( se, selectedClass );
}
break;
}
}
},
/**
* ## removeHoverClass
*
* removes a hover class from an element
*
* @param {Object} e event object
* @return {Void} void
*/
removeHoverClass( e )
{
utils.removeClass( e.target, this.classes.HOVER );
},
/**
* ## removeListeners
*
* removes event listeners from flounder. normally pre unload
*
* @return {Void} void
*/
removeListeners()
{
const refs = this.refs;
this.removeOptionsListeners();
const qsHTML = document.querySelector( 'html' );
const catchBodyClick = this.catchBodyClick;
qsHTML.removeEventListener( 'click', catchBodyClick );
qsHTML.removeEventListener( 'touchend', catchBodyClick );
const select = refs.select;
select.removeEventListener( 'change', this.divertTarget );
select.removeEventListener( 'blur', this.divertTarget );
refs.selected.removeEventListener( 'click', this.toggleList );
refs.flounder.removeEventListener( 'keydown',
this.checkFlounderKeypress );
if ( this.search )
{
this.removeSearchListeners();
}
},
/**
* ## removeMultiTag
*
* removes a multi selection tag on click; fixes all references to value
* and state
*
* @param {Object} e event object
*
* @return {Void} void
*/
removeMultiTag( e )
{
e.preventDefault();
e.stopPropagation();
let value;
let index;
const classes = this.classes;
const refs = this.refs;
const select = refs.select;
const selected = refs.selected;
const target = e.target;
const data = this.refs.data;
const targetIndex = target.getAttribute( 'data-index' );
select[ targetIndex ].selected = false;
const selectedOptions = this.getSelected();
utils.removeClass( data[ targetIndex ], classes.SELECTED_HIDDEN );
utils.removeClass( data[ targetIndex ], classes.SELECTED );
this.hideEmptySection( data[ targetIndex ].parentNode );
target.removeEventListener( 'click', this.removeMultiTag );
const span = target.parentNode;
span.parentNode.removeChild( span );
if ( selectedOptions.length === 0 )
{
this.addPlaceholder();
index = -1;
value = '';
}
else
{
value = selectedOptions.map( option =>
{
return option.value;
} );
index = selectedOptions.map( option =>
{
return option.index;
} );
}
this.removeNoMoreOptionsMessage();
this.fuzzySearchReset();
selected.setAttribute( 'data-value', value );
selected.setAttribute( 'data-index', index );
if ( this.onChange )
{
try
{
this.onChange( e, this.getSelectedValues() );
}
catch ( e )
{
console.warn( 'something may be wrong in "onChange"', e );
}
}
},
/**
* ## removeOptionsListeners
*
* removes event listeners on the data divs
*
* @return {Void} void
*/
removeOptionsListeners()
{
this.refs.data.forEach( dataObj =>
{
if ( dataObj.tagName === 'DIV' )
{
dataObj.removeEventListener( 'click', this.clickSet );
dataObj.removeEventListener( 'mouseenter', this.addHoverClass );
dataObj.removeEventListener( 'mouseleave',
this.removeHoverClass );
}
} );
},
/**
* ## removeNoMoreOptionsMessage
*
* Removing 'No More options' message from the option list
*
* @return {Void} void
*/
removeNoMoreOptionsMessage()
{
const noMoreOptionsEl = this.refs.noMoreOptionsEl;
if ( this.refs.optionsList && noMoreOptionsEl )
{
this.refs.optionsList.removeChild( noMoreOptionsEl );
this.refs.noMoreOptionsEl = undefined;
}
},
/**
* ## removeNoResultsMessage
*
* Removing 'No Results' message from the option list
*
* @return {Void} void
*/
removeNoResultsMessage()
{
const noResultsEl = this.refs.noResultsEl;
if ( this.refs.optionsList && noResultsEl )
{
this.refs.optionsList.removeChild( noResultsEl );
this.refs.noResultsEl = undefined;
}
},
/**
* ## removeSearchListeners
*
* removes the listeners from the search input
*
* @return {Void} void
*/
removeSearchListeners()
{
const search = this.refs.search;
search.removeEventListener( 'click', this.toggleListSearchClick );
search.removeEventListener( 'focus', this.toggleListSearchClick );
search.removeEventListener( 'keyup', this.debouncedFuzzySearch );
search.removeEventListener( 'focus', this.clearPlaceholder );
},
/**
* ## removeSelectedClass
*
* removes the [[this.selectedClass]] from all data and sections
*
* @param {Array} data array of data objects
*
* @param {Array} sections array of section objects
*
* @return {Void} void
*/
removeSelectedClass( data, sections )
{
data = data || this.refs.data;
sections = sections || this.refs.sections;
data.forEach( dataObj =>
{
utils.removeClass( dataObj, this.selectedClass );
} );
sections.forEach( sectionObj =>
{
utils.removeClass( sectionObj, this.selectedClass );
} );
},
/**
* ## removeSelectedValue
*
* sets the selected property to false for all data
*
* @param {Array} data array of data objects
*
* @return {Void} void
*/
removeSelectedValue( data )
{
data = data || this.refs.data;
const optionTags = this.refs.selectOptions;
data.forEach( ( d, i ) =>
{
optionTags[ i ].selected = false;
} );
},
/**
* ## removeSelectKeyListener
*
* disables the event listener on the native select box
*
* @return {Void} void
*/
removeSelectKeyListener()
{
const select = this.refs.select;
select.removeEventListener( 'keyup', this.setSelectValue );
},
/**
* ## setKeypress
*
* handles arrow key and enter selection
*
* @param {Object} e event object
*
* @return {Void} void
*/
setKeypress( e )
{
const refs = this.refs;
let increment = 0;
const keyCode = e.keyCode;
const nonCharacterKeys = keycodes.NON_CHARACTER_KEYS;
if ( nonCharacterKeys.indexOf( keyCode ) === -1 )
{
if ( keyCode === keycodes.TAB )
{
const optionsList = refs.optionsListWrapper;
const wrapper = refs.wrapper;
this.addPlaceholder();
this.toggleClosed( e, optionsList, refs, wrapper, true );
return false;
}
if ( keyCode === keycodes.ENTER || keyCode === keycodes.ESCAPE ||
keyCode === keycodes.SPACE )
{
this.toggleList( e );
return false;
}
if ( keyCode === keycodes.UP || keyCode === keycodes.DOWN )
{
if ( !window.sidebar )
{
e.preventDefault();
if ( refs.search )
{
refs.search.value = '';
}
increment = keyCode - 39;
}
this.setKeypressElement( e, increment );
}
return true;
}
},
/**
* ## setKeypressElement
*
* sets the element after the keypress. if the element is hidden or
* disabled, it passes the event back to setKeypress to process the next
* element
*
* @param {Object} e event object
* @param {Number} increment amount to change the index by
*
* @return {Void} void
*/
setKeypressElement( e, increment )
{
const refs = this.refs;
const selectTag = refs.select;
const data = refs.data;
const dataMaxIndex = data.length - 1;
let index = selectTag.selectedIndex + increment;
if ( index > dataMaxIndex )
{
index = 0;
}
else if ( index < 0 )
{
index = dataMaxIndex;
}
const classes = this.classes;
const hasClass = utils.hasClass;
const dataAtIndex = data[ index ];
selectTag.selectedIndex = index;
if ( hasClass( dataAtIndex, classes.HIDDEN ) ||
hasClass( dataAtIndex, classes.SELECTED_HIDDEN ) ||
hasClass( dataAtIndex, classes.SEARCH_HIDDEN ) ||
hasClass( dataAtIndex, classes.DISABLED ) )
{
this.setKeypress( e );
}
},
/**
* ## setSelectValue
*
* sets the selected value in flounder. when activated by a click,
* the event object is moved to the second variable. this gives us the
* ability to discern between triggered events (keyup) and processed events
* (click) for the sake of choosing our targets
*
* @param {Object} obj possible event object
* @param {Object} e event object
*
* @return {Void} void
*/
setSelectValue( obj, e )
{
const refs = this.refs;
let keyCode;
if ( e ) // click
{
this.setSelectValueClick( e );
}
else // keypress
{
keyCode = obj.keyCode;
this.setSelectValueButton( obj );
}
this.displaySelected( refs.selected, refs );
if ( !this.programmaticClick )
{
// tab, shift, ctrl, alt, caps, cmd
const nonKeys = [ 9, 16, 17, 18, 20, 91 ];
if ( e || obj.type === 'blur' ||
!keyCode && obj.type === 'change' ||
keyCode && nonKeys.indexOf( keyCode ) === -1 )
{
if ( this.toggleList.justOpened && !e )
{
this.toggleList.justOpened = false;
}
else if ( this.onChange )
{
try
{
this.onChange( e, this.getSelectedValues() );
}
catch ( e )
{
console.warn( 'something may be wrong in onChange', e );
}
}
}
this.programmaticClick = false;
}
},
/**
* ## setSelectValueButton
*
* processes the setting of a value after a keypress event
*
* @return {Void} void
*/
setSelectValueButton()
{
const refs = this.refs;
const data = refs.data;
const selectedClass = this.selectedClass;
let selectedOption;
if ( this.multipleTags )
{
return false;
}
this.removeSelectedClass( data );
const dataArray = this.getSelected();
const baseOption = dataArray[ 0 ];
if ( baseOption )
{
selectedOption = data[ baseOption.index ];
utils.addClass( selectedOption, selectedClass );
utils.scrollTo( selectedOption, refs.optionsListWrapper );
}
},
/**
* ## setSelectValueClick
*
* processes the setting of a value after a click event
*
* @param {Object} e event object
*
* @return {Void} void
*/
setSelectValueClick( e )
{
const multiple = this.multiple;
const refs = this.refs;
const selectedClass = this.selectedClass;
const target = e.target;
const index = target.getAttribute( 'data-index' );
const selectedOption = refs.selectOptions[ index ];
if ( ( !multiple || multiple && !this.multipleTags &&
!e[ this.multiSelect ] ) && !this.forceMultiple )
{
this.deselectAll();
utils.addClass( target, selectedClass );
selectedOption.selected = selectedOption.selected !== true;
}
else
{
utils.toggleClass( target, selectedClass );
selectedOption.selected = !selectedOption.selected;
}
this.forceMultiple = false;
this.hideEmptySection( target.parentNode );
const firstOption = refs.selectOptions[ 0 ];
if ( firstOption.value === '' && this.getSelected().length > 1 )
{
utils.removeClass( refs.data[ 0 ], selectedClass );
refs.selectOptions[ 0 ].selected = false;
}
},
/**
* ## toggleClosed
*
* post toggleList, this runs it the list should be closed
*
* @param {Object} e event object
* @param {DOMElement} optionsList the options list
* @param {Object} refs contains the references of the elements in flounder
* @param {DOMElement} wrapper wrapper of flounder
* @param {Boolean} exit prevents refocus. used while tabbing away
*
* @return {Void} void
*/
toggleClosed( e,
optionsList,
refs,
wrapper,
exit = false
)
{
const classes = this.classes;
utils.addClass( refs.optionsListWrapper, classes.HIDDEN );
utils.removeClass( wrapper, classes.OPEN );
const qsHTML = document.querySelector( 'html' );
qsHTML.removeEventListener( 'click', this.catchBodyClick );
qsHTML.removeEventListener( 'touchend', this.catchBodyClick );
if ( this.search )
{
this.fuzzySearchReset();
}
else
{
this.removeSelectKeyListener();
}
if ( !exit )
{
setTimeout( () => refs.flounder.focus(), 0 );
}
if ( this.onClose && this.ready )
{
try
{
this.onClose( e, this.getSelectedValues() );
}
catch ( e )
{
console.warn( 'something may be wrong in "onClose"', e );
}
}
},
/**
* ## toggleList
*
* on click of flounder--selected, this shows or hides the options list
*
* @param {Object} e event object
* @param {String} force toggle can be forced by passing 'open' or 'close'
*
* @return {Void} void
*/
toggleList( e, force )
{
const classes = this.classes;
const refs = this.refs;
const optionsList = refs.optionsListWrapper;
const wrapper = refs.wrapper;
const type = e.type;
const isHidden = utils.hasClass( optionsList, classes.HIDDEN );
if ( type === 'mouseleave' || force === 'close' || !isHidden )
{
this.toggleList.justOpened = false;
this.toggleClosed( e, optionsList, refs, wrapper );
}
else
{
const data = this.data;
let shouldOpen = false;
if ( data.length === 1 )
{
shouldOpen = !( data[ 0 ].extraClass &&
data[ 0 ].extraClass.indexOf( classes.PLACEHOLDER ) > -1 );
}
else if ( data.length > 1 )
{
shouldOpen = true;
}
if ( shouldOpen )
{
if ( type === 'keydown' )
{
this.toggleList.justOpened = true;
}
this.toggleOpen( e, optionsList, refs, wrapper );
}
}
},
/**
* ## toggleListSearchClick
*
* toggleList wrapper for search. only triggered if flounder is closed
*
* @param {Object} e event object
*
* @return {Void} void
*/
toggleListSearchClick( e )
{
const classes = this.classes;
if ( !utils.hasClass( this.refs.wrapper, classes.OPEN ) )
{
this.toggleList( e, 'open' );
}
},
/**
* ## toggleOpen
*
* post toggleList, this runs it the list should be opened
*
* @param {Object} e event object
* @param {DOMElement} optionsList the options list
* @param {Object} refs contains the references of the elements in flounder
*
* @return {Void} void
*/
toggleOpen( e, optionsList, refs )
{
if ( !this.isIos || this.search || this.multipleTags === true )
{
const classes = this.classes;
utils.removeClass( refs.optionsListWrapper, classes.HIDDEN );
utils.addClass( refs.wrapper, classes.OPEN );
const qsHTML = document.querySelector( 'html' );
qsHTML.addEventListener( 'click', this.catchBodyClick );
qsHTML.addEventListener( 'touchend', this.catchBodyClick );
}
if ( !this.multipleTags )
{
const index = refs.select.selectedIndex;
const selectedDiv = refs.data[ index ];
utils.scrollTo( selectedDiv, refs.optionsListWrapper );
}
if ( this.search )
{
setTimeout( () => refs.search.focus(), 0 );
}
else
{
this.addSelectKeyListener();
setTimeout( () => refs.select.focus(), 0 );
}
let optionCount = refs.data.length;
if ( this.props.placeholder )
{
optionCount--;
}
if ( refs.multiTagWrapper )
{
const numTags = refs.multiTagWrapper.children.length - 1;
if ( numTags === optionCount )
{
this.removeNoResultsMessage();
this.addNoMoreOptionsMessage();
}
}
if ( this.onOpen && this.ready )
{
try
{
this.onOpen( e, this.getSelectedValues() );
}
catch ( e )
{
console.warn( 'something may be wrong in "onOpen"', e );
}
}
}
};
export default events;