mirror of
https://github.com/danog/react-datalist-input.git
synced 2024-12-03 09:57:50 +01:00
npm audit fix
This commit is contained in:
commit
e1bf2be3ad
22
README.md
22
README.md
@ -112,7 +112,7 @@ match = (currentInput, item) => {
|
||||
***onDropdownClose***
|
||||
|
||||
- The callback function that will be called after closing the drop down menu.
|
||||
|
||||
|
||||
***placeholder***
|
||||
|
||||
- The placeholder that will be shown inside the input field.
|
||||
@ -167,8 +167,26 @@ match = (currentInput, item) => {
|
||||
|
||||
***initialValue***
|
||||
|
||||
- Specigy an initial value for the input field.
|
||||
- Specify an initial value for the input field.
|
||||
- For example, `initialValue={'hello world'}` will print `hello world` into the input field on first render.
|
||||
- Default is empty string.
|
||||
|
||||
***debounceTime***
|
||||
|
||||
- Use `debounceTime` to define a debounce timeout time (in milliseconds) before the matching algorithm should be called
|
||||
- New user input will trigger a new call to the debounce step and will clear every unresolved timeout
|
||||
- For example, `debounceTime={1000}` will call the matching algorithm one second after the last user input
|
||||
- This is useful if `items` is very large and/or the `match`-algorithm is doing some heavier operations
|
||||
- `debounceTime` may improve the user experience by reducing lag times as it reduces the calls to the matching and rendering of the dropdown.
|
||||
- Be careful, using too much debounceTime will slow down the response time of this component.
|
||||
- If you still have performance issues even when using a `debounceTime={3000}` or higher, you might want to consider using another package / user input instead. Think about a "search/look-up"-button next to your input field or even consider running the search functionality in a dedicated backend.
|
||||
- Default is zero which means no timeout/debouncing is used.
|
||||
|
||||
|
||||
***debounceLoader***
|
||||
|
||||
- Only in use if debounceTime is set
|
||||
- Of type node which can be anything that react can render and will be shown as a loading bar
|
||||
- Default is string "loading...".
|
||||
|
||||
|
||||
|
2
index.d.ts
vendored
2
index.d.ts
vendored
@ -23,6 +23,8 @@ declare module 'react-datalist-input' {
|
||||
initialValue?: string;
|
||||
onDropdownOpen?: () => void;
|
||||
onDropdownClose?: () => void;
|
||||
debounceTime?: number;
|
||||
debounceLoader?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class DataListInput extends React.Component<DataListInputProperties> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-datalist-input",
|
||||
"version": "1.2.11",
|
||||
"version": "1.2.13",
|
||||
"description": "This package provides a react component as follows: an input field with a drop down menu to pick a possible option based on the current input.",
|
||||
"main": "./lib/DataListInput.js",
|
||||
"license": "MIT",
|
||||
|
@ -20,20 +20,25 @@ class DataListInput extends React.Component {
|
||||
visible: false,
|
||||
/* index of the currently focused item in the drop down menu */
|
||||
focusIndex: 0,
|
||||
/* cleaner click events */
|
||||
/* cleaner click events, click interaction within dropdown menu */
|
||||
interactionHappened: false,
|
||||
/* show loader if still matching in debounced mode */
|
||||
isMatchingDebounced: false,
|
||||
};
|
||||
|
||||
/* to manage debouncing of matching, typing input into the input field */
|
||||
this.inputHappenedTimeout = undefined;
|
||||
|
||||
window.addEventListener( 'click', this.onClickCloseMenu, false );
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
const { currentInput, visible } = this.state;
|
||||
const { currentInput, visible, isMatchingDebounced } = this.state;
|
||||
const { initialValue } = this.props;
|
||||
|
||||
// if we have an initialValue, we want to reset it everytime we update and are empty
|
||||
// also setting a new initialValue will trigger this
|
||||
if ( !currentInput && initialValue && !visible ) {
|
||||
if ( !currentInput && initialValue && !visible && !isMatchingDebounced ) {
|
||||
this.setState( { currentInput: initialValue } );
|
||||
}
|
||||
}
|
||||
@ -73,75 +78,6 @@ class DataListInput extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onClickInput = () => {
|
||||
const { visible, lastValidItem } = this.state;
|
||||
let { currentInput } = this.state;
|
||||
const {
|
||||
requiredInputLength, dropDownLength, items, match,
|
||||
clearInputOnSelect, initialValue, onDropdownOpen,
|
||||
} = this.props;
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
|
||||
// if user clicks on input field with initialValue,
|
||||
// the user most likely wants to clear the input field
|
||||
if ( initialValue && currentInput === initialValue ) {
|
||||
this.setState( { currentInput: '' } );
|
||||
currentInput = '';
|
||||
}
|
||||
|
||||
if ( reachedRequiredLength && !visible ) {
|
||||
const matchingItems = items.filter( ( item ) => {
|
||||
if ( typeof ( match ) === typeof ( Function ) ) {
|
||||
return match( currentInput, item );
|
||||
}
|
||||
return this.match( currentInput, item );
|
||||
} );
|
||||
|
||||
const currentInputIsLastItem = !clearInputOnSelect && lastValidItem
|
||||
&& lastValidItem.label === currentInput;
|
||||
const displayableItems = matchingItems.length && !currentInputIsLastItem
|
||||
? matchingItems.slice( 0, dropDownLength ) : items.slice( 0, dropDownLength );
|
||||
|
||||
let index = lastValidItem && !clearInputOnSelect
|
||||
? this.indexOfItem( lastValidItem, displayableItems ) : 0;
|
||||
index = index > 0 ? index : 0;
|
||||
|
||||
this.setState( { visible: true, matchingItems: displayableItems, focusIndex: index },
|
||||
onDropdownOpen );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets called when someone starts to write in the input field
|
||||
* @param value
|
||||
*/
|
||||
onHandleInput = ( event ) => {
|
||||
const currentInput = event.target.value;
|
||||
const {
|
||||
items, match, dropDownLength, onDropdownOpen, onDropdownClose,
|
||||
} = this.props;
|
||||
const matchingItems = items.filter( ( item ) => {
|
||||
if ( typeof ( match ) === typeof ( Function ) ) { return match( currentInput, item ); }
|
||||
return this.match( currentInput, item );
|
||||
} );
|
||||
const displayableItems = matchingItems.slice( 0, dropDownLength );
|
||||
if ( matchingItems.length > 0 ) {
|
||||
this.setState( {
|
||||
currentInput,
|
||||
matchingItems: displayableItems,
|
||||
focusIndex: 0,
|
||||
visible: true,
|
||||
}, onDropdownOpen );
|
||||
} else {
|
||||
this.setState( {
|
||||
currentInput,
|
||||
matchingItems: displayableItems,
|
||||
visible: false,
|
||||
focusIndex: -1,
|
||||
}, onDropdownClose );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* default function for matching the current input value (needle)
|
||||
* and the values of the items array
|
||||
@ -152,6 +88,18 @@ class DataListInput extends React.Component {
|
||||
match = ( currentInput, item ) => item
|
||||
.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
|
||||
|
||||
/**
|
||||
* matching process to find matching entries in items array
|
||||
* @param currentInput
|
||||
* @param item
|
||||
* @param match
|
||||
* @returns {Array}
|
||||
*/
|
||||
matching = ( currentInput, items, match ) => items.filter( ( item ) => {
|
||||
if ( typeof ( match ) === typeof ( Function ) ) { return match( currentInput, item ); }
|
||||
return this.match( currentInput, item );
|
||||
} );
|
||||
|
||||
/**
|
||||
* function for getting the index of the currentValue inside a value of the values array
|
||||
* @param currentInput
|
||||
@ -163,6 +111,87 @@ class DataListInput extends React.Component {
|
||||
|
||||
indexOfItem = ( item, items ) => items.indexOf( items.find( i => i.key === item.key ) )
|
||||
|
||||
/**
|
||||
* runs the matching process of the current input
|
||||
* and handles debouncing the different callback calls to reduce lag time
|
||||
* for bigger datasets or heavier matching algorithms
|
||||
* @param currentInput
|
||||
*/
|
||||
debouncedMatchingUpdateStep = ( currentInput ) => {
|
||||
const { lastValidItem } = this.state;
|
||||
const {
|
||||
items, match, debounceTime, dropDownLength, requiredInputLength,
|
||||
clearInputOnSelect, onDropdownOpen, onDropdownClose,
|
||||
} = this.props;
|
||||
// cleanup waiting update step
|
||||
if ( this.inputHappenedTimeout ) {
|
||||
clearTimeout( this.inputHappenedTimeout );
|
||||
}
|
||||
|
||||
// set currentInput into input field and show loading if debounced mode is on
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
const showMatchingStillLoading = debounceTime >= 0 && reachedRequiredLength;
|
||||
this.setState( { currentInput, isMatchingDebounced: showMatchingStillLoading } );
|
||||
|
||||
// no matching if we do not reach required input length
|
||||
if ( !reachedRequiredLength ) return;
|
||||
|
||||
const updateMatchingItems = () => {
|
||||
const matchingItems = this.matching( currentInput, items, match );
|
||||
const displayableItems = matchingItems.slice( 0, dropDownLength );
|
||||
const showDragIndex = lastValidItem && !clearInputOnSelect;
|
||||
const index = showDragIndex ? this.indexOfItem( lastValidItem, displayableItems ) : 0;
|
||||
if ( matchingItems.length > 0 ) {
|
||||
this.setState( {
|
||||
matchingItems: displayableItems,
|
||||
focusIndex: index > 0 ? index : 0,
|
||||
visible: true,
|
||||
isMatchingDebounced: false,
|
||||
}, onDropdownOpen );
|
||||
} else {
|
||||
this.setState( {
|
||||
matchingItems: displayableItems,
|
||||
visible: false,
|
||||
focusIndex: -1,
|
||||
isMatchingDebounced: false,
|
||||
}, onDropdownClose );
|
||||
}
|
||||
};
|
||||
|
||||
if ( debounceTime <= 0 ) {
|
||||
updateMatchingItems();
|
||||
} else {
|
||||
this.inputHappenedTimeout = setTimeout( updateMatchingItems, debounceTime );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets called when someone starts to write in the input field
|
||||
* @param value
|
||||
*/
|
||||
onHandleInput = ( event ) => {
|
||||
const currentInput = event.target.value;
|
||||
this.debouncedMatchingUpdateStep( currentInput );
|
||||
};
|
||||
|
||||
onClickInput = () => {
|
||||
const { visible } = this.state;
|
||||
let { currentInput } = this.state;
|
||||
const { requiredInputLength, initialValue } = this.props;
|
||||
|
||||
// if user clicks on input field with initialValue,
|
||||
// the user most likely wants to clear the input field
|
||||
if ( initialValue && currentInput === initialValue ) {
|
||||
this.setState( { currentInput: '' } );
|
||||
currentInput = '';
|
||||
}
|
||||
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
if ( reachedRequiredLength && !visible ) {
|
||||
this.debouncedMatchingUpdateStep( currentInput );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle key events
|
||||
* @param event
|
||||
@ -226,7 +255,9 @@ class DataListInput extends React.Component {
|
||||
*/
|
||||
onSelect = ( selectedItem ) => {
|
||||
const { suppressReselect, clearInputOnSelect, onDropdownClose } = this.props;
|
||||
const { lastValidItem } = this.state;
|
||||
const { lastValidItem, isMatchingDebounced } = this.state;
|
||||
// block select call until last matching went through
|
||||
if ( isMatchingDebounced ) return;
|
||||
if ( suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key ) {
|
||||
// do not trigger the callback function
|
||||
// but still change state to fit new selection
|
||||
@ -297,6 +328,12 @@ class DataListInput extends React.Component {
|
||||
</div>
|
||||
);
|
||||
|
||||
renderLoader = ( debounceLoader, dropdownClassName, itemClassName ) => (
|
||||
<div className={`datalist-items ${ dropdownClassName || 'default-datalist-items' }`}>
|
||||
<div className={itemClassName}>{debounceLoader || 'loading...'}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
renderInputField = ( placeholder, currentInput, inputClassName ) => (
|
||||
<input
|
||||
onChange={this.onHandleInput}
|
||||
@ -311,20 +348,26 @@ class DataListInput extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentInput, matchingItems, focusIndex, visible,
|
||||
currentInput, matchingItems, focusIndex, visible, isMatchingDebounced,
|
||||
} = this.state;
|
||||
const {
|
||||
placeholder, inputClassName, activeItemClassName,
|
||||
itemClassName, requiredInputLength, dropdownClassName,
|
||||
placeholder, inputClassName, activeItemClassName, itemClassName,
|
||||
requiredInputLength, dropdownClassName, debounceLoader,
|
||||
} = this.props;
|
||||
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
|
||||
let renderedResults;
|
||||
if ( reachedRequiredLength && isMatchingDebounced ) {
|
||||
renderedResults = this.renderLoader( debounceLoader, itemClassName, dropdownClassName );
|
||||
} else if ( reachedRequiredLength && visible ) {
|
||||
renderedResults = this.renderItems( currentInput, matchingItems, focusIndex,
|
||||
activeItemClassName, itemClassName, dropdownClassName );
|
||||
}
|
||||
return (
|
||||
<div className="datalist-input">
|
||||
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
|
||||
{ reachedRequiredLength && visible
|
||||
&& this.renderItems( currentInput, matchingItems, focusIndex,
|
||||
activeItemClassName, itemClassName, dropdownClassName )
|
||||
}
|
||||
{ renderedResults }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -354,6 +397,8 @@ DataListInput.propTypes = {
|
||||
suppressReselect: PropTypes.bool,
|
||||
dropDownLength: PropTypes.number,
|
||||
initialValue: PropTypes.string,
|
||||
debounceTime: PropTypes.number,
|
||||
debounceLoader: PropTypes.node,
|
||||
};
|
||||
|
||||
DataListInput.defaultProps = {
|
||||
@ -368,6 +413,8 @@ DataListInput.defaultProps = {
|
||||
suppressReselect: true,
|
||||
dropDownLength: Infinity,
|
||||
initialValue: '',
|
||||
debounceTime: 0,
|
||||
debounceLoader: undefined,
|
||||
onDropdownOpen: () => {},
|
||||
onDropdownClose: () => {},
|
||||
};
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import csvFile from './data.csv';
|
||||
|
||||
import './App.css';
|
||||
import DataListInput from './DataListInput';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const data = [
|
||||
{
|
||||
key: '0',
|
||||
@ -18,8 +20,57 @@ const data = [
|
||||
},
|
||||
];
|
||||
|
||||
async function getAndParseData( filename ) {
|
||||
const response = await fetch( filename );
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder( 'utf-8' );
|
||||
const result = await reader.read();
|
||||
const allText = decoder.decode( result.value );
|
||||
|
||||
const allTextLines = allText.split( /\r\n|\n/ );
|
||||
const headers = allTextLines[ 0 ].split( ',' );
|
||||
const lines = [];
|
||||
|
||||
for ( let i = 1; i < allTextLines.length; i += 1 ) {
|
||||
const set = allTextLines[ i ].split( ',' );
|
||||
if ( set.length === headers.length ) {
|
||||
const tarr = {};
|
||||
for ( let j = 0; j < headers.length; j += 1 ) {
|
||||
tarr[ headers[ j ] ] = set[ j ];
|
||||
}
|
||||
lines.push( tarr );
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function annoyinglySlowMatchingAlg( currentInput, item ) {
|
||||
for ( let i = 0; i < 100000; i += 1 ) {
|
||||
i += 1;
|
||||
i -= 1;
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
( currentInput.length + item.label.length ) % 2;
|
||||
}
|
||||
return item.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [ item, setItem ] = useState();
|
||||
const [ items, setItems ] = useState( [] );
|
||||
|
||||
useEffect( () => {
|
||||
getAndParseData( csvFile ).then( obj => setItems( obj
|
||||
.concat( obj )
|
||||
.map( ( row, i ) => (
|
||||
{
|
||||
...row,
|
||||
label: row.vorname,
|
||||
key: i,
|
||||
}
|
||||
) ) ) );
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="content">
|
||||
@ -32,12 +83,14 @@ function App() {
|
||||
}
|
||||
<div className="wrapper">
|
||||
<DataListInput
|
||||
items={data}
|
||||
items={items}
|
||||
onSelect={i => setItem( i )}
|
||||
placeholder="Select a ingredient"
|
||||
clearInputOnSelect={false}
|
||||
suppressReselect={false}
|
||||
initialValue={item ? item.label : ''}
|
||||
debounceTime={1000}
|
||||
debounceLoader={<>Hello</>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,20 +20,25 @@ class DataListInput extends React.Component {
|
||||
visible: false,
|
||||
/* index of the currently focused item in the drop down menu */
|
||||
focusIndex: 0,
|
||||
/* cleaner click events */
|
||||
/* cleaner click events, click interaction within dropdown menu */
|
||||
interactionHappened: false,
|
||||
/* show loader if still matching in debounced mode */
|
||||
isMatchingDebounced: false,
|
||||
};
|
||||
|
||||
/* to manage debouncing of matching, typing input into the input field */
|
||||
this.inputHappenedTimeout = undefined;
|
||||
|
||||
window.addEventListener( 'click', this.onClickCloseMenu, false );
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
const { currentInput, visible } = this.state;
|
||||
const { currentInput, visible, isMatchingDebounced } = this.state;
|
||||
const { initialValue } = this.props;
|
||||
|
||||
// if we have an initialValue, we want to reset it everytime we update and are empty
|
||||
// also setting a new initialValue will trigger this
|
||||
if ( !currentInput && initialValue && !visible ) {
|
||||
if ( !currentInput && initialValue && !visible && !isMatchingDebounced ) {
|
||||
this.setState( { currentInput: initialValue } );
|
||||
}
|
||||
}
|
||||
@ -73,75 +78,6 @@ class DataListInput extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onClickInput = () => {
|
||||
const { visible, lastValidItem } = this.state;
|
||||
let { currentInput } = this.state;
|
||||
const {
|
||||
requiredInputLength, dropDownLength, items, match,
|
||||
clearInputOnSelect, initialValue, onDropdownOpen,
|
||||
} = this.props;
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
|
||||
// if user clicks on input field with initialValue,
|
||||
// the user most likely wants to clear the input field
|
||||
if ( initialValue && currentInput === initialValue ) {
|
||||
this.setState( { currentInput: '' } );
|
||||
currentInput = '';
|
||||
}
|
||||
|
||||
if ( reachedRequiredLength && !visible ) {
|
||||
const matchingItems = items.filter( ( item ) => {
|
||||
if ( typeof ( match ) === typeof ( Function ) ) {
|
||||
return match( currentInput, item );
|
||||
}
|
||||
return this.match( currentInput, item );
|
||||
} );
|
||||
|
||||
const currentInputIsLastItem = !clearInputOnSelect && lastValidItem
|
||||
&& lastValidItem.label === currentInput;
|
||||
const displayableItems = matchingItems.length && !currentInputIsLastItem
|
||||
? matchingItems.slice( 0, dropDownLength ) : items.slice( 0, dropDownLength );
|
||||
|
||||
let index = lastValidItem && !clearInputOnSelect
|
||||
? this.indexOfItem( lastValidItem, displayableItems ) : 0;
|
||||
index = index > 0 ? index : 0;
|
||||
|
||||
this.setState( { visible: true, matchingItems: displayableItems, focusIndex: index },
|
||||
onDropdownOpen );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets called when someone starts to write in the input field
|
||||
* @param value
|
||||
*/
|
||||
onHandleInput = ( event ) => {
|
||||
const currentInput = event.target.value;
|
||||
const {
|
||||
items, match, dropDownLength, onDropdownOpen, onDropdownClose,
|
||||
} = this.props;
|
||||
const matchingItems = items.filter( ( item ) => {
|
||||
if ( typeof ( match ) === typeof ( Function ) ) { return match( currentInput, item ); }
|
||||
return this.match( currentInput, item );
|
||||
} );
|
||||
const displayableItems = matchingItems.slice( 0, dropDownLength );
|
||||
if ( matchingItems.length > 0 ) {
|
||||
this.setState( {
|
||||
currentInput,
|
||||
matchingItems: displayableItems,
|
||||
focusIndex: 0,
|
||||
visible: true,
|
||||
}, onDropdownOpen );
|
||||
} else {
|
||||
this.setState( {
|
||||
currentInput,
|
||||
matchingItems: displayableItems,
|
||||
visible: false,
|
||||
focusIndex: -1,
|
||||
}, onDropdownClose );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* default function for matching the current input value (needle)
|
||||
* and the values of the items array
|
||||
@ -152,6 +88,18 @@ class DataListInput extends React.Component {
|
||||
match = ( currentInput, item ) => item
|
||||
.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
|
||||
|
||||
/**
|
||||
* matching process to find matching entries in items array
|
||||
* @param currentInput
|
||||
* @param item
|
||||
* @param match
|
||||
* @returns {Array}
|
||||
*/
|
||||
matching = ( currentInput, items, match ) => items.filter( ( item ) => {
|
||||
if ( typeof ( match ) === typeof ( Function ) ) { return match( currentInput, item ); }
|
||||
return this.match( currentInput, item );
|
||||
} );
|
||||
|
||||
/**
|
||||
* function for getting the index of the currentValue inside a value of the values array
|
||||
* @param currentInput
|
||||
@ -163,6 +111,87 @@ class DataListInput extends React.Component {
|
||||
|
||||
indexOfItem = ( item, items ) => items.indexOf( items.find( i => i.key === item.key ) )
|
||||
|
||||
/**
|
||||
* runs the matching process of the current input
|
||||
* and handles debouncing the different callback calls to reduce lag time
|
||||
* for bigger datasets or heavier matching algorithms
|
||||
* @param currentInput
|
||||
*/
|
||||
debouncedMatchingUpdateStep = ( currentInput ) => {
|
||||
const { lastValidItem } = this.state;
|
||||
const {
|
||||
items, match, debounceTime, dropDownLength, requiredInputLength,
|
||||
clearInputOnSelect, onDropdownOpen, onDropdownClose,
|
||||
} = this.props;
|
||||
// cleanup waiting update step
|
||||
if ( this.inputHappenedTimeout ) {
|
||||
clearTimeout( this.inputHappenedTimeout );
|
||||
}
|
||||
|
||||
// set currentInput into input field and show loading if debounced mode is on
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
const showMatchingStillLoading = debounceTime >= 0 && reachedRequiredLength;
|
||||
this.setState( { currentInput, isMatchingDebounced: showMatchingStillLoading } );
|
||||
|
||||
// no matching if we do not reach required input length
|
||||
if ( !reachedRequiredLength ) return;
|
||||
|
||||
const updateMatchingItems = () => {
|
||||
const matchingItems = this.matching( currentInput, items, match );
|
||||
const displayableItems = matchingItems.slice( 0, dropDownLength );
|
||||
const showDragIndex = lastValidItem && !clearInputOnSelect;
|
||||
const index = showDragIndex ? this.indexOfItem( lastValidItem, displayableItems ) : 0;
|
||||
if ( matchingItems.length > 0 ) {
|
||||
this.setState( {
|
||||
matchingItems: displayableItems,
|
||||
focusIndex: index > 0 ? index : 0,
|
||||
visible: true,
|
||||
isMatchingDebounced: false,
|
||||
}, onDropdownOpen );
|
||||
} else {
|
||||
this.setState( {
|
||||
matchingItems: displayableItems,
|
||||
visible: false,
|
||||
focusIndex: -1,
|
||||
isMatchingDebounced: false,
|
||||
}, onDropdownClose );
|
||||
}
|
||||
};
|
||||
|
||||
if ( debounceTime <= 0 ) {
|
||||
updateMatchingItems();
|
||||
} else {
|
||||
this.inputHappenedTimeout = setTimeout( updateMatchingItems, debounceTime );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets called when someone starts to write in the input field
|
||||
* @param value
|
||||
*/
|
||||
onHandleInput = ( event ) => {
|
||||
const currentInput = event.target.value;
|
||||
this.debouncedMatchingUpdateStep( currentInput );
|
||||
};
|
||||
|
||||
onClickInput = () => {
|
||||
const { visible } = this.state;
|
||||
let { currentInput } = this.state;
|
||||
const { requiredInputLength, initialValue } = this.props;
|
||||
|
||||
// if user clicks on input field with initialValue,
|
||||
// the user most likely wants to clear the input field
|
||||
if ( initialValue && currentInput === initialValue ) {
|
||||
this.setState( { currentInput: '' } );
|
||||
currentInput = '';
|
||||
}
|
||||
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
if ( reachedRequiredLength && !visible ) {
|
||||
this.debouncedMatchingUpdateStep( currentInput );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle key events
|
||||
* @param event
|
||||
@ -226,7 +255,9 @@ class DataListInput extends React.Component {
|
||||
*/
|
||||
onSelect = ( selectedItem ) => {
|
||||
const { suppressReselect, clearInputOnSelect, onDropdownClose } = this.props;
|
||||
const { lastValidItem } = this.state;
|
||||
const { lastValidItem, isMatchingDebounced } = this.state;
|
||||
// block select call until last matching went through
|
||||
if ( isMatchingDebounced ) return;
|
||||
if ( suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key ) {
|
||||
// do not trigger the callback function
|
||||
// but still change state to fit new selection
|
||||
@ -297,6 +328,12 @@ class DataListInput extends React.Component {
|
||||
</div>
|
||||
);
|
||||
|
||||
renderLoader = ( debounceLoader, dropdownClassName, itemClassName ) => (
|
||||
<div className={`datalist-items ${ dropdownClassName || 'default-datalist-items' }`}>
|
||||
<div className={itemClassName}>{debounceLoader || 'loading...'}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
renderInputField = ( placeholder, currentInput, inputClassName ) => (
|
||||
<input
|
||||
onChange={this.onHandleInput}
|
||||
@ -311,20 +348,26 @@ class DataListInput extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentInput, matchingItems, focusIndex, visible,
|
||||
currentInput, matchingItems, focusIndex, visible, isMatchingDebounced,
|
||||
} = this.state;
|
||||
const {
|
||||
placeholder, inputClassName, activeItemClassName,
|
||||
itemClassName, requiredInputLength, dropdownClassName,
|
||||
placeholder, inputClassName, activeItemClassName, itemClassName,
|
||||
requiredInputLength, dropdownClassName, debounceLoader,
|
||||
} = this.props;
|
||||
|
||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
||||
|
||||
let renderedResults;
|
||||
if ( reachedRequiredLength && isMatchingDebounced ) {
|
||||
renderedResults = this.renderLoader( debounceLoader, itemClassName, dropdownClassName );
|
||||
} else if ( reachedRequiredLength && visible ) {
|
||||
renderedResults = this.renderItems( currentInput, matchingItems, focusIndex,
|
||||
activeItemClassName, itemClassName, dropdownClassName );
|
||||
}
|
||||
return (
|
||||
<div className="datalist-input">
|
||||
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
|
||||
{ reachedRequiredLength && visible
|
||||
&& this.renderItems( currentInput, matchingItems, focusIndex,
|
||||
activeItemClassName, itemClassName, dropdownClassName )
|
||||
}
|
||||
{ renderedResults }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -354,6 +397,8 @@ DataListInput.propTypes = {
|
||||
suppressReselect: PropTypes.bool,
|
||||
dropDownLength: PropTypes.number,
|
||||
initialValue: PropTypes.string,
|
||||
debounceTime: PropTypes.number,
|
||||
debounceLoader: PropTypes.node,
|
||||
};
|
||||
|
||||
DataListInput.defaultProps = {
|
||||
@ -368,6 +413,8 @@ DataListInput.defaultProps = {
|
||||
suppressReselect: true,
|
||||
dropDownLength: Infinity,
|
||||
initialValue: '',
|
||||
debounceTime: 0,
|
||||
debounceLoader: undefined,
|
||||
onDropdownOpen: () => {},
|
||||
onDropdownClose: () => {},
|
||||
};
|
||||
|
7021
testing/demo-app/src/data.csv
Normal file
7021
testing/demo-app/src/data.csv
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user