flounder
Version:
a native friendly dropdown menu
537 lines (457 loc) • 13.3 kB
JavaScript
/*
* Copyright (c) 2016-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 */
import { defaultOptions } from './defaults';
import utils from './utils';
import api from './api';
import build from './build';
import events from './events';
import Search from './search';
import version from './version';
import keycodes from './keycodes';
/**
* main flounder class
*
* @return {Object} Flounder instance
*/
class Flounder
{
/**
* ## componentWillUnmount
*
* on unmount, removes events
*
* @return {Void} void
*/
componentWillUnmount()
{
if ( this.onComponentWillUnmount )
{
try
{
this.onComponentWillUnmount();
}
catch ( e )
{
console.warn(
'something may be wrong in "onComponentWillUnmount"', e );
}
}
this.removeListeners();
if ( this.originalChildren )
{
this.popInSelectElements( this.refs.select );
}
}
/**
* ## constructor
*
* filters and sets up the main init
*
* @param {Mixed} target flounder mount point _DOMElement, String, Array_
* @param {Object} props passed options
*
* @return {Object} new flounder object
*/
constructor( target, props )
{
if ( !target )
{
console.warn( 'Flounder - No target element found.' );
}
else
{
if ( typeof target === 'string' )
{
target = document.querySelectorAll( target );
}
if ( ( target.length || target.length === 0 ) &&
target.tagName !== 'SELECT' )
{
if ( target.length > 1 )
{
console.warn( `Flounder - More than one element found.
Dropping all but the first.` );
}
else if ( target.length === 0 )
{
throw 'Flounder - No target element found.';
}
target = target[ 0 ];
}
if ( target.flounder )
{
target.flounder.destroy();
}
return this.init( target, props );
}
}
/**
* ## filterSearchResults
*
* filters results and adjusts the search hidden class on the dataOptions
*
* @param {Object} e event object
*
* @return {Void} void
*/
filterSearchResults( e )
{
const val = e.target.value.trim();
this.fuzzySearch.previousValue = val;
const matches = this.search.isThereAnythingRelatedTo( val ) || [];
if ( val !== '' )
{
const data = this.refs.data;
const sections = this.refs.sections;
const classes = this.classes;
data.forEach( el =>
{
utils.addClass( el, classes.SEARCH_HIDDEN );
} );
sections.forEach( se =>
{
utils.addClass( se, classes.SEARCH_HIDDEN );
} );
matches.forEach( e =>
{
utils.removeClass( data[ e.i ], classes.SEARCH_HIDDEN );
if ( typeof e.d.s == 'number' )
{
utils.removeClass( sections[ e.d.s ],
classes.SEARCH_HIDDEN );
}
} );
if ( !this.refs.noMoreOptionsEl )
{
if ( matches.length === 0 )
{
this.addNoResultsMessage();
}
else
{
this.removeNoResultsMessage();
}
}
}
else
{
this.fuzzySearchReset();
}
}
/**
* ## fuzzySearch
*
* filters events to determine the correct actions, based on events from
* the search box
*
* @param {Object} e event object
*
* @return {Void} void
*/
fuzzySearch( e )
{
this.fuzzySearch.previousValue = this.fuzzySearch.previousValue || '';
if ( this.onInputChange )
{
try
{
this.onInputChange( e );
}
catch ( e )
{
console.warn( 'something may be wrong in "onInputChange"', e );
}
}
if ( !this.toggleList.justOpened )
{
e.preventDefault();
const keyCode = e.keyCode;
if ( keyCode !== keycodes.UP && keyCode !== keycodes.DOWN &&
keyCode !== keycodes.ENTER && keyCode !== keycodes.ESCAPE )
{
if ( this.multipleTags && keyCode === keycodes.BACKSPACE &&
this.fuzzySearch.previousValue === '' )
{
const lastTag = this.refs.search.previousSibling;
if ( lastTag )
{
setTimeout( () => lastTag.focus(), 0 );
}
}
else
{
this.filterSearchResults( e );
}
}
else if ( keyCode === keycodes.ESCAPE ||
keyCode === keycodes.ENTER )
{
this.fuzzySearchReset();
this.toggleList( e, 'close' );
this.addPlaceholder();
}
}
else
{
this.toggleList.justOpened = false;
}
}
/**
* ## fuzzySearchReset
*
* resets all options to visible
*
* @return {Void} void
*/
fuzzySearchReset()
{
const refs = this.refs;
const classes = this.classes;
refs.sections.forEach( se =>
{
utils.removeClass( se, classes.SEARCH_HIDDEN );
} );
refs.data.forEach( dataObj =>
{
utils.removeClass( dataObj, classes.SEARCH_HIDDEN );
} );
refs.search.value = '';
this.removeNoResultsMessage();
}
/**
* ## init
*
* post setup, this sets initial values and starts the build process
*
* @param {DOMElement} target flounder mount point
* @param {Object} props passed options
*
* @return {Object} new flounder object
*/
init( target, props )
{
this.props = props;
this.bindThis();
this.initializeOptions();
this.setTarget( target );
if ( this.search )
{
this.search = new Search( this );
}
if ( this.onInit )
{
try
{
this.onInit();
}
catch ( e )
{
console.warn( 'something may be wrong in "onInit"', e );
}
}
this.buildDom();
const { isOsx, isIos, multiSelect } = utils.setPlatform();
this.isOsx = isOsx;
this.isIos = isIos;
this.multiSelect = multiSelect;
this.onRender();
if ( this.onComponentDidMount )
{
try
{
this.onComponentDidMount();
}
catch ( e )
{
console.warn(
'something may be wrong in onComponentDidMount', e );
}
}
this.ready = true;
this.originalTarget.flounder = this.target.flounder = this;
return this.refs.flounder.flounder = this;
}
/**
* ## initializeOptions
*
* inserts the initial options into the flounder object, setting defaults
* when necessary
*
* @return {Void} void
*/
initializeOptions()
{
const props = this.props = this.props || {};
for ( const opt in defaultOptions )
{
// depreciated @todo remove @2.0.0
if ( opt === 'onChange' && props.onSelect )
{
this.onChange = props.onSelect;
console.warn( `Please use onChange. onSelect has been
depricated and will be removed in 2.0.0` );
this.onSelect = function()
{
console.warn( `Please use onChange. onSelect has been
depricated and will be removed in 2.0.0` );
this.onChange( ...arguments );
};
}
else if ( opt === 'classes' )
{
const defaultClasses = defaultOptions.classes;
const propClasses = typeof props.classes === 'object' ?
props.classes : {};
this.classes = {};
for ( const clss in defaultClasses )
{
const propClass = propClasses[ clss ];
const buildClass = [ defaultClasses[ clss ] ];
if ( propClass && propClass !== buildClass[ 0 ] )
{
buildClass.push( propClass );
}
this.classes[ clss ] = buildClass;
}
}
else if ( opt === 'data' )
{
this.data = props.data && props.data.length ?
[ ...props.data ] :
[ ...defaultOptions.data ];
}
else
{
this[ opt ] = props[ opt ] !== undefined ? props[ opt ] :
defaultOptions[ opt ];
}
}
this.selectedClass = this.classes.SELECTED;
if ( props.defaultEmpty )
{
this.placeholder = '';
}
if ( this.multipleTags )
{
this.search = true;
this.multiple = true;
this.selectedClass.push( this.classes.SELECTED_HIDDEN );
}
}
/**
* ## onRender
*
* attaches necessary events to the built DOM
*
* @return {Void} void
*/
onRender()
{
const props = this.props;
const refs = this.refs;
if ( !!this.isIos && !this.multiple )
{
const sel = refs.select;
const classes = this.classes;
utils.removeClass( sel, classes.HIDDEN );
utils.addClass( sel, classes.HIDDEN_IOS );
}
this.addListeners( refs, props );
}
/**
* ## sortData
*
* checks the data object for header options, and sorts it accordingly
*
* @param {Array} data flounder data options
* @param {Array} res results
* @param {Number} i index
* @param {Number} s section's index (undefined = no section)
*
* @return {Boolean} hasHeaders
*/
sortData( data, res = [], i = 0, s = undefined )
{
let indexHeader = 0;
data.forEach( d =>
{
if ( d.header )
{
res = this.sortData( d.data, res, i, indexHeader );
indexHeader++;
}
else
{
/* istanbul ignore next */
if ( typeof d !== 'object' )
{
d = {
text : d,
value : d,
index : i
};
}
else
{
d.index = i;
}
if ( s !== undefined )
{
d.s = s;
}
res.push( d );
i++;
}
} );
return res;
}
}
/**
* ## .find
*
* accepts array-like objects and selector strings to make multiple flounders
*
* @param {Mixed} targets target(s) _Array or String_
* @param {Object} props passed options
*
* @return {Array} array of flounders
*/
Flounder.find = function( targets, props )
{
if ( typeof targets === 'string' )
{
targets = document.querySelectorAll( targets );
}
else if ( targets.nodeType === 1 )
{
targets = [ targets ];
}
return Array.prototype.slice.call( targets, 0 )
.map( el => new Flounder( el, props ) );
};
/**
* ## version
*
* sets version with getters and no setters for the sake of being read-only
*/
Object.defineProperty( Flounder, 'version', {
get : function()
{
return version;
}
} );
Object.defineProperty( Flounder.prototype, 'version', {
get : function()
{
return version;
}
} );
utils.extendClass( Flounder, api, build, events );
export default Flounder;