react-datalist-input
Advanced tools
Comparing version 1.3.13 to 2.0.0
@@ -23,3 +23,4 @@ | ||
var React = _interopDefault(require('react')); | ||
var React = require('react'); | ||
var React__default = _interopDefault(React); | ||
var PropTypes = _interopDefault(require('prop-types')); | ||
@@ -43,534 +44,427 @@ | ||
function _classCallCheck(instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new TypeError("Cannot call a class as a function"); | ||
} | ||
function _slicedToArray(arr, i) { | ||
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); | ||
} | ||
function _defineProperties(target, props) { | ||
for (var i = 0; i < props.length; i++) { | ||
var descriptor = props[i]; | ||
descriptor.enumerable = descriptor.enumerable || false; | ||
descriptor.configurable = true; | ||
if ("value" in descriptor) descriptor.writable = true; | ||
Object.defineProperty(target, descriptor.key, descriptor); | ||
} | ||
function _arrayWithHoles(arr) { | ||
if (Array.isArray(arr)) return arr; | ||
} | ||
function _createClass(Constructor, protoProps, staticProps) { | ||
if (protoProps) _defineProperties(Constructor.prototype, protoProps); | ||
if (staticProps) _defineProperties(Constructor, staticProps); | ||
return Constructor; | ||
} | ||
function _iterableToArrayLimit(arr, i) { | ||
if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; | ||
var _arr = []; | ||
var _n = true; | ||
var _d = false; | ||
var _e = undefined; | ||
function _defineProperty(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
try { | ||
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { | ||
_arr.push(_s.value); | ||
return obj; | ||
} | ||
function _inherits(subClass, superClass) { | ||
if (typeof superClass !== "function" && superClass !== null) { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
if (i && _arr.length === i) break; | ||
} | ||
} catch (err) { | ||
_d = true; | ||
_e = err; | ||
} finally { | ||
try { | ||
if (!_n && _i["return"] != null) _i["return"](); | ||
} finally { | ||
if (_d) throw _e; | ||
} | ||
} | ||
subClass.prototype = Object.create(superClass && superClass.prototype, { | ||
constructor: { | ||
value: subClass, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
if (superClass) _setPrototypeOf(subClass, superClass); | ||
return _arr; | ||
} | ||
function _getPrototypeOf(o) { | ||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { | ||
return o.__proto__ || Object.getPrototypeOf(o); | ||
}; | ||
return _getPrototypeOf(o); | ||
function _unsupportedIterableToArray(o, minLen) { | ||
if (!o) return; | ||
if (typeof o === "string") return _arrayLikeToArray(o, minLen); | ||
var n = Object.prototype.toString.call(o).slice(8, -1); | ||
if (n === "Object" && o.constructor) n = o.constructor.name; | ||
if (n === "Map" || n === "Set") return Array.from(o); | ||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); | ||
} | ||
function _setPrototypeOf(o, p) { | ||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { | ||
o.__proto__ = p; | ||
return o; | ||
}; | ||
function _arrayLikeToArray(arr, len) { | ||
if (len == null || len > arr.length) len = arr.length; | ||
return _setPrototypeOf(o, p); | ||
} | ||
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; | ||
function _isNativeReflectConstruct() { | ||
if (typeof Reflect === "undefined" || !Reflect.construct) return false; | ||
if (Reflect.construct.sham) return false; | ||
if (typeof Proxy === "function") return true; | ||
try { | ||
Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
return arr2; | ||
} | ||
function _assertThisInitialized(self) { | ||
if (self === void 0) { | ||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); | ||
} | ||
return self; | ||
function _nonIterableRest() { | ||
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); | ||
} | ||
function _possibleConstructorReturn(self, call) { | ||
if (call && (typeof call === "object" || typeof call === "function")) { | ||
return call; | ||
} | ||
var useStateRef = function useStateRef(initalState) { | ||
var _useState = React.useState(initalState), | ||
_useState2 = _slicedToArray(_useState, 2), | ||
state = _useState2[0], | ||
setState = _useState2[1]; | ||
return _assertThisInitialized(self); | ||
} | ||
var ref = React.useRef(initalState); | ||
function _createSuper(Derived) { | ||
var hasNativeReflectConstruct = _isNativeReflectConstruct(); | ||
var setStateRef = function setStateRef(newState) { | ||
setState(newState); | ||
ref.current = newState; | ||
}; | ||
return function _createSuperInternal() { | ||
var Super = _getPrototypeOf(Derived), | ||
result; | ||
return [state, setStateRef, ref]; | ||
}; | ||
if (hasNativeReflectConstruct) { | ||
var NewTarget = _getPrototypeOf(this).constructor; | ||
result = Reflect.construct(Super, arguments, NewTarget); | ||
} else { | ||
result = Super.apply(this, arguments); | ||
} | ||
return _possibleConstructorReturn(this, result); | ||
}; | ||
} | ||
___$insertStyle(".datalist-input {\n /*the container must be positioned relative:*/\n position: relative;\n display: inline-block;\n width: 100%;\n}\n\n.datalist-input .autocomplete-input {\n width: 100%;\n}\n\n.datalist-input .datalist-items {\n position: absolute;\n z-index: 99;\n /*position the autocomplete items to be the same width as the container:*/\n top: 100%;\n left: 0;\n right: 0;\n}\n\n.datalist-input .default-datalist-items {\n border: 1px solid #d4d4d4;\n border-bottom: none;\n border-top: none;\n}\n\n.datalist-input .default-datalist-items div:not(.datalist-active-item) {\n padding: 10px;\n cursor: pointer;\n background-color: #fff;\n border-bottom: 1px solid #d4d4d4;\n}\n\n.datalist-input .default-datalist-items div:not(.datalist-active-item):hover {\n /*when hovering an item:*/\n background-color: #e9e9e9;\n}\n\n.datalist-input .datalist-active-item {\n /*when navigating through the items using the arrow keys:*/\n cursor: pointer;\n}\n\n.datalist-input .datalist-active-item-default {\n background-color: DodgerBlue;\n color: #ffffff;\n border-bottom: 1px solid #d4d4d4;\n padding: 10px;\n}"); | ||
var DataListInput = /*#__PURE__*/function (_React$Component) { | ||
_inherits(DataListInput, _React$Component); | ||
/** | ||
* default function for matching the current input value (needle) | ||
* and the values of the items array | ||
* @param currentInput | ||
* @param item | ||
* @returns {boolean} | ||
*/ | ||
var _super = _createSuper(DataListInput); | ||
var labelMatch = function labelMatch(currentInput, item) { | ||
return item.label.substr(0, currentInput.length).toLowerCase() === currentInput.toLowerCase(); | ||
}; | ||
/** | ||
* function for getting the index of the currentValue inside a value of the values array | ||
* @param currentInput | ||
* @param item | ||
* @returns {number} | ||
*/ | ||
function DataListInput(props) { | ||
var _this; | ||
_classCallCheck(this, DataListInput); | ||
var indexOfMatch = function indexOfMatch(currentInput, item) { | ||
return item.label.toLowerCase().indexOf(currentInput.toLowerCase()); | ||
}; | ||
/** | ||
* index of item in items | ||
* @param {*} item | ||
* @param {*} items | ||
*/ | ||
_this = _super.call(this, props); | ||
_defineProperty(_assertThisInitialized(_this), "componentDidMount", function () { | ||
if (typeof window !== 'undefined') { | ||
window.addEventListener('click', _this.onClickCloseMenu, false); | ||
} | ||
}); | ||
var indexOfItem = function indexOfItem(item, items) { | ||
return items.indexOf(items.find(function (i) { | ||
return i.key === item.key; | ||
})); | ||
}; | ||
_defineProperty(_assertThisInitialized(_this), "componentDidUpdate", function () { | ||
var _this$state = _this.state, | ||
currentInput = _this$state.currentInput, | ||
visible = _this$state.visible, | ||
isMatchingDebounced = _this$state.isMatchingDebounced; | ||
var initialValue = _this.props.initialValue; // if we have an initialValue, we want to reset it everytime we update and are empty | ||
// also setting a new initialValue will trigger this | ||
var DataListInput = function DataListInput(_ref) { | ||
var activeItemClassName = _ref.activeItemClassName, | ||
clearInputOnSelect = _ref.clearInputOnSelect, | ||
debounceLoader = _ref.debounceLoader, | ||
debounceTime = _ref.debounceTime, | ||
dropdownClassName = _ref.dropdownClassName, | ||
dropDownLength = _ref.dropDownLength, | ||
initialValue = _ref.initialValue, | ||
inputClassName = _ref.inputClassName, | ||
itemClassName = _ref.itemClassName, | ||
match = _ref.match, | ||
onDropdownClose = _ref.onDropdownClose, | ||
onDropdownOpen = _ref.onDropdownOpen, | ||
onInput = _ref.onInput, | ||
onSelect = _ref.onSelect, | ||
placeholder = _ref.placeholder, | ||
requiredInputLength = _ref.requiredInputLength, | ||
suppressReselect = _ref.suppressReselect, | ||
items = _ref.items; | ||
if (!currentInput && initialValue && !visible && !isMatchingDebounced) { | ||
_this.setState({ | ||
currentInput: initialValue | ||
}); | ||
} | ||
}); | ||
/* last valid item that was selected from the drop down menu */ | ||
var _useState = React.useState(), | ||
_useState2 = _slicedToArray(_useState, 2), | ||
lastValidItem = _useState2[0], | ||
setLastValidItem = _useState2[1]; | ||
/* current input text */ | ||
_defineProperty(_assertThisInitialized(_this), "componentWillUnmount", function () { | ||
if (typeof window !== 'undefined') { | ||
window.removeEventListener('click', _this.onClickCloseMenu); | ||
} | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onClickCloseMenu", function (event) { | ||
var menu = document.getElementsByClassName('datalist-items'); | ||
if (!menu || !menu.length) return; // if rerender, items inside might change, allow one click without further checking | ||
var _useStateRef = useStateRef(initialValue), | ||
_useStateRef2 = _slicedToArray(_useStateRef, 3), | ||
currentInput = _useStateRef2[0], | ||
setCurrentInput = _useStateRef2[1], | ||
currentInputRef = _useStateRef2[2]; | ||
/* current set of matching items */ | ||
var interactionHappened = _this.state.interactionHappened; | ||
if (interactionHappened) { | ||
_this.setState({ | ||
interactionHappened: false | ||
}); | ||
var _useState3 = React.useState([]), | ||
_useState4 = _slicedToArray(_useState3, 2), | ||
matchingItems = _useState4[0], | ||
setMatchingItems = _useState4[1]; | ||
/* visibility property of the drop down menu */ | ||
return; | ||
} // do not do anything if input is clicked, as we have a dedicated func for that | ||
var _useStateRef3 = useStateRef(false), | ||
_useStateRef4 = _slicedToArray(_useStateRef3, 3), | ||
visible = _useStateRef4[0], | ||
setVisible = _useStateRef4[1], | ||
visibleRef = _useStateRef4[2]; | ||
/* index of the currently focused item in the drop down menu */ | ||
var input = document.getElementsByClassName('autocomplete-input'); | ||
if (!input) return; | ||
for (var i = 0; i < input.length; i += 1) { | ||
var targetIsInput = event.target === input[i]; | ||
var targetInInput = input[i].contains(event.target); | ||
if (targetIsInput || targetInInput) return; | ||
} // do not close menu if user clicked inside | ||
var _useState5 = React.useState(0), | ||
_useState6 = _slicedToArray(_useState5, 2), | ||
focusIndex = _useState6[0], | ||
setFocusIndex = _useState6[1]; | ||
/* cleaner click events, click interaction within dropdown menu */ | ||
for (var _i = 0; _i < menu.length; _i += 1) { | ||
var targetInMenu = menu[_i].contains(event.target); | ||
var interactionHappenedRef = React.useRef(false); | ||
/* show loader if still matching in debounced mode */ | ||
var targetIsMenu = event.target === menu[_i]; | ||
if (targetInMenu || targetIsMenu) return; | ||
} | ||
var _useState7 = React.useState(false), | ||
_useState8 = _slicedToArray(_useState7, 2), | ||
isMatchingDebounced = _useState8[0], | ||
setIsMatchingDebounced = _useState8[1]; | ||
/* to manage debouncing of matching, typing input into the input field */ | ||
var visible = _this.state.visible; | ||
var onDropdownClose = _this.props.onDropdownClose; | ||
if (visible) { | ||
_this.setState({ | ||
visible: false, | ||
focusIndex: -1 | ||
}, onDropdownClose); | ||
} | ||
}); | ||
var inputHappenedTimeout = React.useRef(); | ||
var menu = React.useRef(); | ||
var inputField = React.useRef(); | ||
React.useEffect(function () { | ||
var onClickCloseMenu = function onClickCloseMenu(event) { | ||
if (!menu.current) return; // if rerender, items inside might change, allow one click without further checking | ||
_defineProperty(_assertThisInitialized(_this), "match", function (currentInput, item) { | ||
return item.label.substr(0, currentInput.length).toUpperCase() === currentInput.toUpperCase(); | ||
}); | ||
if (interactionHappenedRef.current) { | ||
interactionHappenedRef.current = false; | ||
return; | ||
} // do not do anything if input is clicked, as we have a dedicated func for that | ||
_defineProperty(_assertThisInitialized(_this), "matching", function (currentInput, items, match) { | ||
return items.filter(function (item) { | ||
if (_typeof(match) === (typeof Function === "undefined" ? "undefined" : _typeof(Function))) { | ||
return match(currentInput, item); | ||
} | ||
return _this.match(currentInput, item); | ||
}); | ||
}); | ||
if (!inputField.current) return; | ||
var targetIsInput = event.target === inputField.current; | ||
var targetInInput = inputField.current.contains(event.target); | ||
if (targetIsInput || targetInInput) return; // do not close menu if user clicked inside | ||
_defineProperty(_assertThisInitialized(_this), "indexOfMatch", function (currentInput, item) { | ||
return item.label.toUpperCase().indexOf(currentInput.toUpperCase()); | ||
}); | ||
var targetInMenu = menu.current.contains(event.target); | ||
var targetIsMenu = event.target === menu.current; | ||
if (targetInMenu || targetIsMenu) return; | ||
_defineProperty(_assertThisInitialized(_this), "indexOfItem", function (item, items) { | ||
return items.indexOf(items.find(function (i) { | ||
return i.key === item.key; | ||
})); | ||
}); | ||
if (visibleRef.current) { | ||
setVisible(false); | ||
setFocusIndex(-1); | ||
onDropdownClose(); | ||
} | ||
}; | ||
_defineProperty(_assertThisInitialized(_this), "debouncedMatchingUpdateStep", function (currentInput) { | ||
var lastValidItem = _this.state.lastValidItem; | ||
var _this$props = _this.props, | ||
items = _this$props.items, | ||
match = _this$props.match, | ||
debounceTime = _this$props.debounceTime, | ||
dropDownLength = _this$props.dropDownLength, | ||
requiredInputLength = _this$props.requiredInputLength, | ||
clearInputOnSelect = _this$props.clearInputOnSelect, | ||
onDropdownOpen = _this$props.onDropdownOpen, | ||
onDropdownClose = _this$props.onDropdownClose; // cleanup waiting update step && be ssr safe | ||
window.addEventListener('click', onClickCloseMenu, false); | ||
return function () { | ||
window.removeEventListener('click', onClickCloseMenu); | ||
}; | ||
}, [onDropdownClose, setVisible, visibleRef]); | ||
React.useEffect(function () { | ||
// 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 && !isMatchingDebounced) { | ||
setCurrentInput(initialValue); | ||
} | ||
}, [currentInput, visible, isMatchingDebounced, initialValue, setCurrentInput]); | ||
/** | ||
* 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 nextInput | ||
*/ | ||
if (_this.inputHappenedTimeout && typeof window !== 'undefined') { | ||
window.clearTimeout(_this.inputHappenedTimeout); | ||
_this.inputHappenedTimeout = null; | ||
} // set currentInput into input field and show loading if debounced mode is on | ||
var debouncedMatchingUpdateStep = React.useCallback(function (nextInput) { | ||
// cleanup waiting update step | ||
if (inputHappenedTimeout.current) { | ||
clearTimeout(inputHappenedTimeout.current); | ||
inputHappenedTimeout.current = null; | ||
} // set nextInput into input field and show loading if debounced mode is on | ||
var reachedRequiredLength = currentInput.length >= requiredInputLength; | ||
var showMatchingStillLoading = debounceTime >= 0 && reachedRequiredLength; | ||
var reachedRequiredLength = nextInput.length >= requiredInputLength; | ||
var showMatchingStillLoading = debounceTime >= 0 && reachedRequiredLength; | ||
setCurrentInput(nextInput); | ||
setIsMatchingDebounced(showMatchingStillLoading); // no matching if we do not reach required input length | ||
_this.setState({ | ||
currentInput: currentInput, | ||
isMatchingDebounced: showMatchingStillLoading | ||
}); // no matching if we do not reach required input length | ||
if (!reachedRequiredLength) return; | ||
var updateMatchingItems = function updateMatchingItems() { | ||
// matching process to find matching entries in items array | ||
var updatedMatchingItems = items.filter(function (item) { | ||
if (_typeof(match) === (typeof Function === "undefined" ? "undefined" : _typeof(Function))) return match(nextInput, item); | ||
return labelMatch(nextInput, item); | ||
}); | ||
var displayableItems = updatedMatchingItems.slice(0, dropDownLength); | ||
var showDragIndex = lastValidItem && !clearInputOnSelect; | ||
var index = showDragIndex ? indexOfItem(lastValidItem, displayableItems) : 0; | ||
if (!reachedRequiredLength) return; | ||
var updateMatchingItems = function updateMatchingItems() { | ||
var matchingItems = _this.matching(currentInput, items, match); | ||
var displayableItems = matchingItems.slice(0, dropDownLength); | ||
var showDragIndex = lastValidItem && !clearInputOnSelect; | ||
var 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 (displayableItems.length) { | ||
if (!visibleRef.current) { | ||
onDropdownOpen(); | ||
} | ||
}; | ||
if (debounceTime <= 0) { | ||
updateMatchingItems(); | ||
setMatchingItems(displayableItems); | ||
setFocusIndex(index > 0 ? index : 0); | ||
setIsMatchingDebounced(false); | ||
setVisible(true); | ||
} else { | ||
if (typeof window !== 'undefined') { | ||
_this.inputHappenedTimeout = window.setTimeout(updateMatchingItems, debounceTime); | ||
if (visibleRef.current) { | ||
setVisible(false); | ||
onDropdownClose(); | ||
} | ||
setMatchingItems(displayableItems); | ||
setFocusIndex(-1); | ||
setIsMatchingDebounced(false); | ||
} | ||
}); | ||
}; | ||
_defineProperty(_assertThisInitialized(_this), "onHandleInput", function (event) { | ||
var onInput = _this.props.onInput; | ||
var currentInput = event.target.value; | ||
if (debounceTime <= 0) { | ||
updateMatchingItems(); | ||
} else { | ||
inputHappenedTimeout.current = setTimeout(updateMatchingItems, debounceTime); | ||
} | ||
}, [requiredInputLength, debounceTime, setCurrentInput, items, dropDownLength, lastValidItem, clearInputOnSelect, match, setVisible, onDropdownOpen, visibleRef, onDropdownClose]); | ||
/** | ||
* gets called when someone starts to write in the input field | ||
* @param value | ||
*/ | ||
_this.debouncedMatchingUpdateStep(currentInput); | ||
var onHandleInput = React.useCallback(function (event) { | ||
var value = event.target.value; | ||
debouncedMatchingUpdateStep(value); | ||
onInput(value); | ||
}, [debouncedMatchingUpdateStep, onInput]); | ||
var onClickInput = React.useCallback(function () { | ||
var value = currentInputRef.current; // if user clicks on input field with initialValue, | ||
// the user most likely wants to clear the input field | ||
onInput(currentInput); | ||
}); | ||
if (initialValue && value === initialValue) { | ||
value = ''; | ||
} | ||
_defineProperty(_assertThisInitialized(_this), "onClickInput", function () { | ||
var visible = _this.state.visible; | ||
var currentInput = _this.state.currentInput; | ||
var _this$props2 = _this.props, | ||
requiredInputLength = _this$props2.requiredInputLength, | ||
initialValue = _this$props2.initialValue; // if user clicks on input field with initialValue, | ||
// the user most likely wants to clear the input field | ||
var reachedRequiredLength = value.length >= requiredInputLength; | ||
if (initialValue && currentInput === initialValue) { | ||
_this.setState({ | ||
currentInput: '' | ||
}); | ||
if (reachedRequiredLength && !visibleRef.current) { | ||
debouncedMatchingUpdateStep(value); | ||
} | ||
}, [currentInputRef, initialValue, requiredInputLength, visibleRef, debouncedMatchingUpdateStep]); | ||
/** | ||
* handleSelect is called onClickItem and onEnter upon an option of the drop down menu | ||
* does nothing if the key has not changed since the last onSelect event | ||
* @param selectedItem | ||
*/ | ||
currentInput = ''; | ||
} | ||
var onHandleSelect = React.useCallback(function (selectedItem) { | ||
// block select call until last matching went through | ||
if (isMatchingDebounced) return; | ||
setCurrentInput(clearInputOnSelect ? '' : selectedItem.label); | ||
setVisible(false); | ||
setFocusIndex(-1); | ||
interactionHappenedRef.current = true; | ||
onDropdownClose(); | ||
var reachedRequiredLength = currentInput.length >= requiredInputLength; | ||
if (suppressReselect && lastValidItem && selectedItem.key === lastValidItem.key) { | ||
// do not trigger the callback function | ||
// but still change state to fit new selection | ||
return; | ||
} // change state to fit new selection | ||
if (reachedRequiredLength && !visible) { | ||
_this.debouncedMatchingUpdateStep(currentInput); | ||
} | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onHandleKeydown", function (event) { | ||
var _this$state2 = _this.state, | ||
visible = _this$state2.visible, | ||
focusIndex = _this$state2.focusIndex, | ||
matchingItems = _this$state2.matchingItems; // only do something if drop-down div is visible | ||
setLastValidItem(selectedItem); // callback function onSelect | ||
if (!visible) return; | ||
var currentFocusIndex = focusIndex; | ||
onSelect(selectedItem); | ||
}, [isMatchingDebounced, setCurrentInput, clearInputOnSelect, setVisible, onDropdownClose, suppressReselect, lastValidItem, onSelect]); | ||
/** | ||
* handle key events | ||
* @param event | ||
*/ | ||
if (event.keyCode === 40 || event.keyCode === 9) { | ||
// If the arrow DOWN key or tab is pressed increase the currentFocus variable: | ||
currentFocusIndex += 1; | ||
if (currentFocusIndex >= matchingItems.length) currentFocusIndex = 0; | ||
var onHandleKeydown = React.useCallback(function (event) { | ||
// only do something if drop-down div is visible | ||
if (!visibleRef.current) return; | ||
var currentFocusIndex = focusIndex; | ||
_this.setState({ | ||
focusIndex: currentFocusIndex | ||
}); // prevent tab to jump to the next input field if drop down is still open | ||
if (event.keyCode === 40 || event.keyCode === 9) { | ||
// If the arrow DOWN key or tab is pressed increase the currentFocus variable: | ||
currentFocusIndex += 1; | ||
if (currentFocusIndex >= matchingItems.length) currentFocusIndex = 0; | ||
setFocusIndex(currentFocusIndex); // prevent tab to jump to the next input field if drop down is still open | ||
event.preventDefault(); | ||
} else if (event.keyCode === 38) { | ||
// If the arrow UP key is pressed, decrease the currentFocus variable: | ||
currentFocusIndex -= 1; | ||
if (currentFocusIndex <= -1) currentFocusIndex = matchingItems.length - 1; | ||
_this.setState({ | ||
focusIndex: currentFocusIndex | ||
}); | ||
} else if (event.keyCode === 13) { | ||
// Enter pressed, similar to onClickItem | ||
if (focusIndex > -1) { | ||
// Simulate a click on the "active" item: | ||
var selectedItem = matchingItems[currentFocusIndex]; | ||
_this.onSelect(selectedItem); | ||
} | ||
event.preventDefault(); | ||
} else if (event.keyCode === 38) { | ||
// If the arrow UP key is pressed, decrease the currentFocus variable: | ||
currentFocusIndex -= 1; | ||
if (currentFocusIndex <= -1) currentFocusIndex = matchingItems.length - 1; | ||
setFocusIndex(currentFocusIndex); | ||
} else if (event.keyCode === 13) { | ||
// Enter pressed, similar to onClickItem | ||
if (focusIndex > -1) { | ||
// Simulate a click on the "active" item: | ||
var selectedItem = matchingItems[currentFocusIndex]; | ||
onHandleSelect(selectedItem); | ||
} | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onClickItem", function (event) { | ||
var matchingItems = _this.state.matchingItems; // update the input value and close the dropdown again | ||
var elements = event.currentTarget.children; | ||
var selectedKey; | ||
for (var i = 0; i < elements.length; i += 1) { | ||
if (elements[i].tagName === 'INPUT') { | ||
selectedKey = elements[i].value; | ||
break; | ||
} | ||
}, [focusIndex, matchingItems, onHandleSelect, visibleRef]); | ||
var renderItemLabel = React.useCallback(function (item) { | ||
var index = indexOfMatch(currentInput, item); | ||
var inputLength = currentInput.length; | ||
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, index >= 0 && inputLength ? | ||
/*#__PURE__*/ | ||
// renders label with matching search string marked | ||
React__default.createElement(React__default.Fragment, null, item.label.substr(0, index), /*#__PURE__*/React__default.createElement("strong", null, item.label.substr(index, inputLength)), item.label.substr(index + inputLength, item.label.length)) : item.label); | ||
}, [currentInput]); | ||
var renderItems = React.useCallback(function () { | ||
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, matchingItems.map(function (item, i) { | ||
var isActive = focusIndex === i; | ||
var itemActiveClasses = isActive ? "datalist-active-item ".concat(activeItemClassName || 'datalist-active-item-default') : ''; | ||
var itemClasses = "".concat(itemClassName, " ").concat(itemActiveClasses); | ||
return /*#__PURE__*/React__default.createElement("div", { | ||
onClick: function onClick() { | ||
return onHandleSelect(item); | ||
}, | ||
className: itemClasses, | ||
key: item.key, | ||
tabIndex: 0, | ||
role: "button", | ||
"aria-label": item.label, | ||
onKeyUp: function onKeyUp(event) { | ||
return event.preventDefault(); | ||
} | ||
} // key can either be number or string | ||
// eslint-disable-next-line eqeqeq | ||
}, renderItemLabel(item)); | ||
})); | ||
}, [matchingItems, focusIndex, activeItemClassName, itemClassName, onHandleSelect, renderItemLabel]); | ||
var dropDown = React.useMemo(function () { | ||
var reachedRequiredLength = currentInputRef.current.length >= requiredInputLength; | ||
var selectedItem = matchingItems.find(function (item) { | ||
return item.key == selectedKey; | ||
}); | ||
_this.onSelect(selectedItem); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onSelect", function (selectedItem) { | ||
var _this$props3 = _this.props, | ||
suppressReselect = _this$props3.suppressReselect, | ||
clearInputOnSelect = _this$props3.clearInputOnSelect, | ||
onDropdownClose = _this$props3.onDropdownClose; | ||
var _this$state3 = _this.state, | ||
lastValidItem = _this$state3.lastValidItem, | ||
isMatchingDebounced = _this$state3.isMatchingDebounced; // 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 | ||
_this.setState({ | ||
currentInput: clearInputOnSelect ? '' : selectedItem.label, | ||
visible: false, | ||
focusIndex: -1, | ||
interactionHappened: true | ||
}, onDropdownClose); | ||
return; | ||
} // change state to fit new selection | ||
_this.setState({ | ||
currentInput: clearInputOnSelect ? '' : selectedItem.label, | ||
lastValidItem: selectedItem, | ||
visible: false, | ||
focusIndex: -1, | ||
interactionHappened: true | ||
}, onDropdownClose); // callback function onSelect | ||
var onSelect = _this.props.onSelect; | ||
onSelect(selectedItem); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "renderMatchingLabel", function (currentInput, item, indexOfMatch) { | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, item.label.substr(0, indexOfMatch), /*#__PURE__*/React.createElement("strong", null, item.label.substr(indexOfMatch, currentInput.length)), item.label.substr(indexOfMatch + currentInput.length, item.label.length)); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "renderItemLabel", function (currentInput, item, indexOfMatch) { | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, indexOfMatch >= 0 && currentInput.length ? _this.renderMatchingLabel(currentInput, item, indexOfMatch) : item.label); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "renderItems", function (currentInput, items, focusIndex, activeItemClassName, itemClassName, dropdownClassName) { | ||
return /*#__PURE__*/React.createElement("div", { | ||
className: "datalist-items ".concat(dropdownClassName || 'default-datalist-items') | ||
}, items.map(function (item, i) { | ||
var isActive = focusIndex === i; | ||
var itemActiveClasses = isActive ? "datalist-active-item ".concat(activeItemClassName || 'datalist-active-item-default') : ''; | ||
var itemClasses = "".concat(itemClassName, " ").concat(itemActiveClasses); | ||
return /*#__PURE__*/React.createElement("div", { | ||
onClick: _this.onClickItem, | ||
className: itemClasses, | ||
key: item.key, | ||
tabIndex: 0, | ||
role: "button", | ||
onKeyUp: function onKeyUp(event) { | ||
return event.preventDefault(); | ||
} | ||
}, _this.renderItemLabel(currentInput, item, _this.indexOfMatch(currentInput, item)), /*#__PURE__*/React.createElement("input", { | ||
type: "hidden", | ||
value: item.key, | ||
readOnly: true | ||
})); | ||
})); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "renderLoader", function (debounceLoader, dropdownClassName, itemClassName) { | ||
return /*#__PURE__*/React.createElement("div", { | ||
className: "datalist-items ".concat(dropdownClassName || 'default-datalist-items') | ||
}, /*#__PURE__*/React.createElement("div", { | ||
if (reachedRequiredLength && isMatchingDebounced) { | ||
return /*#__PURE__*/React__default.createElement("div", { | ||
ref: menu, | ||
className: "datalist-items ".concat(dropdownClassName || 'default-datalist-items'), | ||
role: "dialog", | ||
"aria-label": "Dropdown menu" | ||
}, /*#__PURE__*/React__default.createElement("div", { | ||
className: itemClassName | ||
}, debounceLoader || 'loading...')); | ||
}); | ||
} | ||
_defineProperty(_assertThisInitialized(_this), "renderInputField", function (placeholder, currentInput, inputClassName) { | ||
return /*#__PURE__*/React.createElement("input", { | ||
onChange: _this.onHandleInput, | ||
onClick: _this.onClickInput, | ||
onKeyDown: _this.onHandleKeydown, | ||
type: "text", | ||
className: "autocomplete-input ".concat(inputClassName), | ||
placeholder: placeholder, | ||
value: currentInput | ||
}); | ||
}); | ||
var _initialValue = _this.props.initialValue; | ||
_this.state = { | ||
/* last valid item that was selected from the drop down menu */ | ||
lastValidItem: undefined, | ||
/* current input text */ | ||
currentInput: _initialValue, | ||
/* current set of matching items */ | ||
matchingItems: [], | ||
/* visibility property of the drop down menu */ | ||
visible: false, | ||
/* index of the currently focused item in the drop down menu */ | ||
focusIndex: 0, | ||
/* 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; | ||
return _this; | ||
} | ||
_createClass(DataListInput, [{ | ||
key: "render", | ||
value: function render() { | ||
var _this$state4 = this.state, | ||
currentInput = _this$state4.currentInput, | ||
matchingItems = _this$state4.matchingItems, | ||
focusIndex = _this$state4.focusIndex, | ||
visible = _this$state4.visible, | ||
isMatchingDebounced = _this$state4.isMatchingDebounced; | ||
var _this$props4 = this.props, | ||
placeholder = _this$props4.placeholder, | ||
inputClassName = _this$props4.inputClassName, | ||
activeItemClassName = _this$props4.activeItemClassName, | ||
itemClassName = _this$props4.itemClassName, | ||
requiredInputLength = _this$props4.requiredInputLength, | ||
dropdownClassName = _this$props4.dropdownClassName, | ||
debounceLoader = _this$props4.debounceLoader; | ||
var reachedRequiredLength = currentInput.length >= requiredInputLength; | ||
var renderedResults; | ||
if (reachedRequiredLength && isMatchingDebounced) { | ||
renderedResults = this.renderLoader(debounceLoader, itemClassName, dropdownClassName); | ||
} else if (reachedRequiredLength && visible) { | ||
renderedResults = this.renderItems(currentInput, matchingItems, focusIndex, activeItemClassName, itemClassName, dropdownClassName); | ||
} | ||
return /*#__PURE__*/React.createElement("div", { | ||
className: "datalist-input" | ||
}, this.renderInputField(placeholder, currentInput, inputClassName), renderedResults); | ||
if (reachedRequiredLength && visible) { | ||
return /*#__PURE__*/React__default.createElement("div", { | ||
ref: menu, | ||
className: "datalist-items ".concat(dropdownClassName || 'default-datalist-items'), | ||
role: "dialog", | ||
"aria-label": "Dropdown menu" | ||
}, renderItems()); | ||
} | ||
}]); | ||
return DataListInput; | ||
}(React.Component); | ||
return undefined; | ||
}, [currentInputRef, requiredInputLength, isMatchingDebounced, visible, dropdownClassName, itemClassName, debounceLoader, renderItems]); | ||
return /*#__PURE__*/React__default.createElement("div", { | ||
className: "datalist-input" | ||
}, /*#__PURE__*/React__default.createElement("input", { | ||
ref: inputField, | ||
onChange: onHandleInput, | ||
onClick: onClickInput, | ||
onKeyDown: onHandleKeydown, | ||
type: "text", | ||
className: "autocomplete-input ".concat(inputClassName), | ||
placeholder: placeholder, | ||
value: currentInput, | ||
"aria-label": "Search" | ||
}), dropDown); | ||
}; | ||
@@ -577,0 +471,0 @@ DataListInput.propTypes = { |
{ | ||
"name": "react-datalist-input", | ||
"version": "1.3.13", | ||
"version": "2.0.0", | ||
"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.", | ||
@@ -37,2 +37,4 @@ "main": "./dist/DataListInput.js", | ||
"@babel/plugin-proposal-class-properties": "^7.10.1", | ||
"@babel/plugin-transform-regenerator": "^7.10.4", | ||
"@babel/plugin-transform-runtime": "^7.11.5", | ||
"@babel/preset-env": "^7.10.2", | ||
@@ -42,7 +44,16 @@ "@babel/preset-react": "^7.10.1", | ||
"@testing-library/react": "^10.2.1", | ||
"eslint": "^7.2.0", | ||
"eslint-config-airbnb": "^18.1.0", | ||
"eslint-plugin-import": "^2.21.2", | ||
"babel-eslint": "^9.0.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb": "^18.2.0", | ||
"eslint-config-prettier": "^4.3.0", | ||
"eslint-config-wesbos": "0.0.22", | ||
"eslint-plugin-html": "^6.0.3", | ||
"eslint-plugin-import": "^2.22.0", | ||
"eslint-plugin-jest-dom": "^3.0.1", | ||
"eslint-plugin-jsx-a11y": "^6.3.1", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"eslint-plugin-react": "^7.20.6", | ||
"eslint-plugin-react-hooks": "^1.7.0", | ||
"jest": "^26.0.1", | ||
"prettier": "^1.19.1", | ||
"prop-types": "^15.7.2", | ||
@@ -49,0 +60,0 @@ "react": "^16.13.1", |
159
README.md
## Info | ||
This package provides a single react component. The component contains an input field with a drop down menu to pick a possible option based on the current input as a react component. | ||
This package provides a single React component. The component contains an input field with a drop down menu to pick a possible option based on the current input as a React component. | ||
Have a look at [w3schools.com](https://www.w3schools.com/howto/howto_js_autocomplete.asp) to see how you can do something similar with pure html, css, and js. For more information about react and the ecosystem see this [guide](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). | ||
Have a look at [w3schools.com](https://www.w3schools.com/howto/howto_js_autocomplete.asp) to see how you can do something similar with pure html, css, and js. For more information about React and the ecosystem see this [guide](https://reactjs.org/docs/getting-started.html). | ||
@@ -15,3 +15,3 @@ ## Demo | ||
### Using Gatsby or Next.js? | ||
### Using Gatsby or Next.js? | ||
@@ -22,2 +22,9 @@ This component is not compatible with server-side rendering since it has css bundled with it. | ||
## Versions | ||
- Version 2.x.x serves a functional component using hooks | ||
- Version 1.x.x serves a class component | ||
The documentation below mainly applies for both versions but will be updated based on version 2.x.x updates in the future. | ||
## Installation | ||
@@ -28,3 +35,3 @@ | ||
```bash | ||
npm install react-datalist-input --save | ||
npm i react-datalist-input | ||
``` | ||
@@ -35,51 +42,42 @@ | ||
```javascript | ||
import DataListInput from 'react-datalist-input'; | ||
import React, { useState, useMemo, useCallback } from "react"; | ||
import DataListInput from "react-datalist-input"; | ||
/** | ||
* OPTIONAL, this packages comes with a simple default label matching function | ||
* but feel free to create your own match algorithm if you want to do so | ||
* @param {String} currentInput (the current user input) | ||
* @param {object} item (one item of the items array) | ||
* @returns {boolean} | ||
*/ | ||
matchCurrentInput = (currentInput, item) => { | ||
const yourLogic = item.someAdditionalValue; | ||
return (yourLogic.substr(0, currentInput.length).toUpperCase() === currentInput.toUpperCase()); | ||
}; | ||
const YourComponent = ({ myValues }) => { | ||
// selectedItem | ||
const [item, setItem] = useState(); | ||
/** | ||
* your callback function gets called if the user selects one option out of the drop down menu | ||
* @param selectedItem object (the selected item / option) | ||
* @returns {*} | ||
*/ | ||
onSelect = (selectedItem) => { | ||
this.doSomething(selectedItem); | ||
}; | ||
/** | ||
* your callback function gets called if the user selects one option out of the drop down menu | ||
* @param selectedItem object (the selected item / option) | ||
*/ | ||
const onSelect = useCallback((selectedItem) => { | ||
console.log("selectedItem", selectedItem); | ||
}, []); | ||
render() { | ||
// the array you want to pass to the react-data-list component | ||
// each element at least needs a key and a label | ||
const items = myValues.map((item, i) => { | ||
return { | ||
// what to show to the user | ||
label: item.id + ": " + item.name, | ||
// key to identify the item within the array | ||
key: item.id, | ||
// feel free to add your own app logic to access those properties in the onSelect function | ||
someAdditionalValue: item.someAdditionalValue, | ||
// or just keep everything | ||
...item, | ||
} | ||
}); | ||
// the array you want to pass to the react-data-list component | ||
// key and label are required properties | ||
const items = useMemo( | ||
() => | ||
myValues.map((oneItem) => ({ | ||
// required: what to show to the user | ||
label: oneItem.name, | ||
// required: key to identify the item within the array | ||
key: oneItem.id, | ||
// feel free to add your own app logic to access those properties in the onSelect function | ||
someAdditionalValue: oneItem.someAdditionalValue, | ||
// or just keep everything | ||
...oneItem, | ||
})), | ||
[myValues] | ||
); | ||
return( | ||
<div> | ||
<DataListInput | ||
placeholder={"Select an option from the drop down menu..."} | ||
items={items} | ||
onSelect={this.onSelect} | ||
match={this.matchCurrentInput} | ||
/> | ||
</div> | ||
); | ||
return ( | ||
<DataListInput | ||
placeholder="Select an option from the drop down menu..." | ||
items={items} | ||
onSelect={onSelect} | ||
/> | ||
); | ||
}; | ||
``` | ||
@@ -89,22 +87,22 @@ | ||
| Prop | Type | Required/Optional | Default Value | ||
|----------|------------- |------| ------| | ||
| [items](#markdown-header-items) | array | required | - | | ||
| [onSelect](#markdown-header-onSelect) | function | required | - | | ||
| [match](#markdown-header-match) | function | optional | internal matching function | | ||
| [onDropdownOpen](#markdown-header-onDropdownOpen) | function | optional | - | | ||
| [onDropdownClose](#markdown-header-onDropdownClose) | function | optional | - | | ||
| [placeholder](#markdown-header-placeholder) | string | optional | '' | | ||
| [itemClassName](#markdown-header-itemClassName) | string | optional | - | | ||
| [activeItemClassName](#markdown-header-activeItemClassName) | string | optional | - | | ||
| [inputClassName](#markdown-header-inputClassName) | string | optional | - | | ||
| [dropdownClassName](#markdown-header-dropdownClassName) | string | optional | - | | ||
| [requiredInputLength](#markdown-header-requiredInputLength) | number | optional | 0 | | ||
| [clearInputOnSelect](#markdown-header-clearInputOnSelect) | boolean | optional | false | | ||
| [suppressReselect](#markdown-header-suppressReselect) | boolean | optional | true | | ||
| [dropDownLength](#markdown-header-dropDownLength) | number | optional | infinite | | ||
| [initialValue](#markdown-header-initialValue) | string | optional | - | | ||
| [debounceTime](#markdown-header-debounceTime) | number | optional | 0 | | ||
| [debounceLoader](#markdown-header-debounceLoader) | string | optional | 'Loading...' | | ||
| [onInput](#markdown-header-onInput) | function | optional | - | | ||
| Prop | Type | Required/Optional | Default Value | | ||
| ----------------------------------------------------------- | -------- | ----------------- | -------------------------- | | ||
| [items](#markdown-header-items) | array | required | - | | ||
| [onSelect](#markdown-header-onSelect) | function | required | - | | ||
| [match](#markdown-header-match) | function | optional | internal matching function | | ||
| [onDropdownOpen](#markdown-header-onDropdownOpen) | function | optional | - | | ||
| [onDropdownClose](#markdown-header-onDropdownClose) | function | optional | - | | ||
| [placeholder](#markdown-header-placeholder) | string | optional | '' | | ||
| [itemClassName](#markdown-header-itemClassName) | string | optional | - | | ||
| [activeItemClassName](#markdown-header-activeItemClassName) | string | optional | - | | ||
| [inputClassName](#markdown-header-inputClassName) | string | optional | - | | ||
| [dropdownClassName](#markdown-header-dropdownClassName) | string | optional | - | | ||
| [requiredInputLength](#markdown-header-requiredInputLength) | number | optional | 0 | | ||
| [clearInputOnSelect](#markdown-header-clearInputOnSelect) | boolean | optional | false | | ||
| [suppressReselect](#markdown-header-suppressReselect) | boolean | optional | true | | ||
| [dropDownLength](#markdown-header-dropDownLength) | number | optional | infinite | | ||
| [initialValue](#markdown-header-initialValue) | string | optional | - | | ||
| [debounceTime](#markdown-header-debounceTime) | number | optional | 0 | | ||
| [debounceLoader](#markdown-header-debounceLoader) | string | optional | 'Loading...' | | ||
| [onInput](#markdown-header-onInput) | function | optional | - | | ||
@@ -116,4 +114,4 @@ ### <a name="markdown-header-items"></a>items | ||
- Every item inside the array needs to have following properties: | ||
- key : an id that identifies the item within the array | ||
- label: the label that will be shown in the drop down menu | ||
- key : an id that identifies the item within the array | ||
- label: the label that will be shown in the drop down menu | ||
@@ -126,3 +124,3 @@ ### <a name="markdown-header-onSelect"></a>onSelect | ||
- Parameter: (selectedKey) | ||
- selectedKey: the Key Property of the item that the user selected | ||
- selectedKey: the Key Property of the item that the user selected | ||
@@ -133,6 +131,8 @@ ### <a name="markdown-header-match"></a>match | ||
- Parameter: (currentInput, item) | ||
- currentInput: String, the current user input typed into the input field | ||
- item: Object, the item of the items array (with key and label properties) | ||
- currentInput: String, the current user input typed into the input field | ||
- item: Object, the item of the items array (with key and label properties) | ||
- Default: | ||
```javascript | ||
@@ -146,3 +146,6 @@ /** | ||
match = (currentInput, item) => { | ||
return item.label.substr(0, currentInput.length).toUpperCase() === currentInput.toUpperCase(); | ||
return ( | ||
item.label.substr(0, currentInput.length).toUpperCase() === | ||
currentInput.toUpperCase() | ||
); | ||
}; | ||
@@ -154,2 +157,3 @@ ``` | ||
- The callback function that will be called after opening the drop down menu. | ||
- It will fire only once and not be called again after new input. | ||
@@ -218,3 +222,3 @@ ### <a name="markdown-header-onDropdownClose"></a>onDropdownClose | ||
- Caution: Don't confuse this with a placeholder (see placerholder prop), this is an actual value in the input | ||
and supports uses cases like saving user state or suggesting a search value. | ||
and supports uses cases like saving user state or suggesting a search value. | ||
@@ -232,3 +236,2 @@ ### <a name="markdown-header-debounceTime"></a>debounceTime | ||
### <a name="markdown-header-debounceLoader"></a>debounceLoader | ||
@@ -244,3 +247,1 @@ | ||
- Exposing this function supports use cases like resetting states on empty input field | ||
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
235
32702
30
437
1