mirror of
https://github.com/danog/react-datalist-input.git
synced 2024-12-04 02:17: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***
|
***onDropdownClose***
|
||||||
|
|
||||||
- The callback function that will be called after closing the drop down menu.
|
- The callback function that will be called after closing the drop down menu.
|
||||||
|
|
||||||
***placeholder***
|
***placeholder***
|
||||||
|
|
||||||
- The placeholder that will be shown inside the input field.
|
- The placeholder that will be shown inside the input field.
|
||||||
@ -167,8 +167,26 @@ match = (currentInput, item) => {
|
|||||||
|
|
||||||
***initialValue***
|
***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.
|
- For example, `initialValue={'hello world'}` will print `hello world` into the input field on first render.
|
||||||
- Default is empty string.
|
- 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;
|
initialValue?: string;
|
||||||
onDropdownOpen?: () => void;
|
onDropdownOpen?: () => void;
|
||||||
onDropdownClose?: () => void;
|
onDropdownClose?: () => void;
|
||||||
|
debounceTime?: number;
|
||||||
|
debounceLoader?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DataListInput extends React.Component<DataListInputProperties> {
|
export default class DataListInput extends React.Component<DataListInputProperties> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-datalist-input",
|
"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.",
|
"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",
|
"main": "./lib/DataListInput.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -20,20 +20,25 @@ class DataListInput extends React.Component {
|
|||||||
visible: false,
|
visible: false,
|
||||||
/* index of the currently focused item in the drop down menu */
|
/* index of the currently focused item in the drop down menu */
|
||||||
focusIndex: 0,
|
focusIndex: 0,
|
||||||
/* cleaner click events */
|
/* cleaner click events, click interaction within dropdown menu */
|
||||||
interactionHappened: false,
|
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 );
|
window.addEventListener( 'click', this.onClickCloseMenu, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = () => {
|
componentDidUpdate = () => {
|
||||||
const { currentInput, visible } = this.state;
|
const { currentInput, visible, isMatchingDebounced } = this.state;
|
||||||
const { initialValue } = this.props;
|
const { initialValue } = this.props;
|
||||||
|
|
||||||
// if we have an initialValue, we want to reset it everytime we update and are empty
|
// if we have an initialValue, we want to reset it everytime we update and are empty
|
||||||
// also setting a new initialValue will trigger this
|
// also setting a new initialValue will trigger this
|
||||||
if ( !currentInput && initialValue && !visible ) {
|
if ( !currentInput && initialValue && !visible && !isMatchingDebounced ) {
|
||||||
this.setState( { currentInput: initialValue } );
|
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)
|
* default function for matching the current input value (needle)
|
||||||
* and the values of the items array
|
* and the values of the items array
|
||||||
@ -152,6 +88,18 @@ class DataListInput extends React.Component {
|
|||||||
match = ( currentInput, item ) => item
|
match = ( currentInput, item ) => item
|
||||||
.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
|
.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
|
* function for getting the index of the currentValue inside a value of the values array
|
||||||
* @param currentInput
|
* @param currentInput
|
||||||
@ -163,6 +111,87 @@ class DataListInput extends React.Component {
|
|||||||
|
|
||||||
indexOfItem = ( item, items ) => items.indexOf( items.find( i => i.key === item.key ) )
|
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
|
* handle key events
|
||||||
* @param event
|
* @param event
|
||||||
@ -226,7 +255,9 @@ class DataListInput extends React.Component {
|
|||||||
*/
|
*/
|
||||||
onSelect = ( selectedItem ) => {
|
onSelect = ( selectedItem ) => {
|
||||||
const { suppressReselect, clearInputOnSelect, onDropdownClose } = this.props;
|
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 ) {
|
if ( suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key ) {
|
||||||
// do not trigger the callback function
|
// do not trigger the callback function
|
||||||
// but still change state to fit new selection
|
// but still change state to fit new selection
|
||||||
@ -297,6 +328,12 @@ class DataListInput extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
renderLoader = ( debounceLoader, dropdownClassName, itemClassName ) => (
|
||||||
|
<div className={`datalist-items ${ dropdownClassName || 'default-datalist-items' }`}>
|
||||||
|
<div className={itemClassName}>{debounceLoader || 'loading...'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
renderInputField = ( placeholder, currentInput, inputClassName ) => (
|
renderInputField = ( placeholder, currentInput, inputClassName ) => (
|
||||||
<input
|
<input
|
||||||
onChange={this.onHandleInput}
|
onChange={this.onHandleInput}
|
||||||
@ -311,20 +348,26 @@ class DataListInput extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
currentInput, matchingItems, focusIndex, visible,
|
currentInput, matchingItems, focusIndex, visible, isMatchingDebounced,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
placeholder, inputClassName, activeItemClassName,
|
placeholder, inputClassName, activeItemClassName, itemClassName,
|
||||||
itemClassName, requiredInputLength, dropdownClassName,
|
requiredInputLength, dropdownClassName, debounceLoader,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
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 (
|
return (
|
||||||
<div className="datalist-input">
|
<div className="datalist-input">
|
||||||
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
|
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
|
||||||
{ reachedRequiredLength && visible
|
{ renderedResults }
|
||||||
&& this.renderItems( currentInput, matchingItems, focusIndex,
|
|
||||||
activeItemClassName, itemClassName, dropdownClassName )
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -354,6 +397,8 @@ DataListInput.propTypes = {
|
|||||||
suppressReselect: PropTypes.bool,
|
suppressReselect: PropTypes.bool,
|
||||||
dropDownLength: PropTypes.number,
|
dropDownLength: PropTypes.number,
|
||||||
initialValue: PropTypes.string,
|
initialValue: PropTypes.string,
|
||||||
|
debounceTime: PropTypes.number,
|
||||||
|
debounceLoader: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
DataListInput.defaultProps = {
|
DataListInput.defaultProps = {
|
||||||
@ -368,6 +413,8 @@ DataListInput.defaultProps = {
|
|||||||
suppressReselect: true,
|
suppressReselect: true,
|
||||||
dropDownLength: Infinity,
|
dropDownLength: Infinity,
|
||||||
initialValue: '',
|
initialValue: '',
|
||||||
|
debounceTime: 0,
|
||||||
|
debounceLoader: undefined,
|
||||||
onDropdownOpen: () => {},
|
onDropdownOpen: () => {},
|
||||||
onDropdownClose: () => {},
|
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 './App.css';
|
||||||
import DataListInput from './DataListInput';
|
import DataListInput from './DataListInput';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
key: '0',
|
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() {
|
function App() {
|
||||||
const [ item, setItem ] = useState();
|
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 (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
@ -32,12 +83,14 @@ function App() {
|
|||||||
}
|
}
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<DataListInput
|
<DataListInput
|
||||||
items={data}
|
items={items}
|
||||||
onSelect={i => setItem( i )}
|
onSelect={i => setItem( i )}
|
||||||
placeholder="Select a ingredient"
|
placeholder="Select a ingredient"
|
||||||
clearInputOnSelect={false}
|
clearInputOnSelect={false}
|
||||||
suppressReselect={false}
|
suppressReselect={false}
|
||||||
initialValue={item ? item.label : ''}
|
initialValue={item ? item.label : ''}
|
||||||
|
debounceTime={1000}
|
||||||
|
debounceLoader={<>Hello</>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,20 +20,25 @@ class DataListInput extends React.Component {
|
|||||||
visible: false,
|
visible: false,
|
||||||
/* index of the currently focused item in the drop down menu */
|
/* index of the currently focused item in the drop down menu */
|
||||||
focusIndex: 0,
|
focusIndex: 0,
|
||||||
/* cleaner click events */
|
/* cleaner click events, click interaction within dropdown menu */
|
||||||
interactionHappened: false,
|
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 );
|
window.addEventListener( 'click', this.onClickCloseMenu, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = () => {
|
componentDidUpdate = () => {
|
||||||
const { currentInput, visible } = this.state;
|
const { currentInput, visible, isMatchingDebounced } = this.state;
|
||||||
const { initialValue } = this.props;
|
const { initialValue } = this.props;
|
||||||
|
|
||||||
// if we have an initialValue, we want to reset it everytime we update and are empty
|
// if we have an initialValue, we want to reset it everytime we update and are empty
|
||||||
// also setting a new initialValue will trigger this
|
// also setting a new initialValue will trigger this
|
||||||
if ( !currentInput && initialValue && !visible ) {
|
if ( !currentInput && initialValue && !visible && !isMatchingDebounced ) {
|
||||||
this.setState( { currentInput: initialValue } );
|
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)
|
* default function for matching the current input value (needle)
|
||||||
* and the values of the items array
|
* and the values of the items array
|
||||||
@ -152,6 +88,18 @@ class DataListInput extends React.Component {
|
|||||||
match = ( currentInput, item ) => item
|
match = ( currentInput, item ) => item
|
||||||
.label.substr( 0, currentInput.length ).toUpperCase() === currentInput.toUpperCase();
|
.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
|
* function for getting the index of the currentValue inside a value of the values array
|
||||||
* @param currentInput
|
* @param currentInput
|
||||||
@ -163,6 +111,87 @@ class DataListInput extends React.Component {
|
|||||||
|
|
||||||
indexOfItem = ( item, items ) => items.indexOf( items.find( i => i.key === item.key ) )
|
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
|
* handle key events
|
||||||
* @param event
|
* @param event
|
||||||
@ -226,7 +255,9 @@ class DataListInput extends React.Component {
|
|||||||
*/
|
*/
|
||||||
onSelect = ( selectedItem ) => {
|
onSelect = ( selectedItem ) => {
|
||||||
const { suppressReselect, clearInputOnSelect, onDropdownClose } = this.props;
|
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 ) {
|
if ( suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key ) {
|
||||||
// do not trigger the callback function
|
// do not trigger the callback function
|
||||||
// but still change state to fit new selection
|
// but still change state to fit new selection
|
||||||
@ -297,6 +328,12 @@ class DataListInput extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
renderLoader = ( debounceLoader, dropdownClassName, itemClassName ) => (
|
||||||
|
<div className={`datalist-items ${ dropdownClassName || 'default-datalist-items' }`}>
|
||||||
|
<div className={itemClassName}>{debounceLoader || 'loading...'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
renderInputField = ( placeholder, currentInput, inputClassName ) => (
|
renderInputField = ( placeholder, currentInput, inputClassName ) => (
|
||||||
<input
|
<input
|
||||||
onChange={this.onHandleInput}
|
onChange={this.onHandleInput}
|
||||||
@ -311,20 +348,26 @@ class DataListInput extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
currentInput, matchingItems, focusIndex, visible,
|
currentInput, matchingItems, focusIndex, visible, isMatchingDebounced,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
placeholder, inputClassName, activeItemClassName,
|
placeholder, inputClassName, activeItemClassName, itemClassName,
|
||||||
itemClassName, requiredInputLength, dropdownClassName,
|
requiredInputLength, dropdownClassName, debounceLoader,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const reachedRequiredLength = currentInput.length >= requiredInputLength;
|
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 (
|
return (
|
||||||
<div className="datalist-input">
|
<div className="datalist-input">
|
||||||
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
|
{ this.renderInputField( placeholder, currentInput, inputClassName ) }
|
||||||
{ reachedRequiredLength && visible
|
{ renderedResults }
|
||||||
&& this.renderItems( currentInput, matchingItems, focusIndex,
|
|
||||||
activeItemClassName, itemClassName, dropdownClassName )
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -354,6 +397,8 @@ DataListInput.propTypes = {
|
|||||||
suppressReselect: PropTypes.bool,
|
suppressReselect: PropTypes.bool,
|
||||||
dropDownLength: PropTypes.number,
|
dropDownLength: PropTypes.number,
|
||||||
initialValue: PropTypes.string,
|
initialValue: PropTypes.string,
|
||||||
|
debounceTime: PropTypes.number,
|
||||||
|
debounceLoader: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
DataListInput.defaultProps = {
|
DataListInput.defaultProps = {
|
||||||
@ -368,6 +413,8 @@ DataListInput.defaultProps = {
|
|||||||
suppressReselect: true,
|
suppressReselect: true,
|
||||||
dropDownLength: Infinity,
|
dropDownLength: Infinity,
|
||||||
initialValue: '',
|
initialValue: '',
|
||||||
|
debounceTime: 0,
|
||||||
|
debounceLoader: undefined,
|
||||||
onDropdownOpen: () => {},
|
onDropdownOpen: () => {},
|
||||||
onDropdownClose: () => {},
|
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