react-custom-chips
Version:
A very lightweight and custom react chips
411 lines (389 loc) • 21.3 kB
JavaScript
;
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var React = require('react');
var React__default = _interopDefault(React);
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __spreadArrays() {
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
for (var r = Array(s), k = 0, i = 0; i < il; i++)
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
r[k] = a[j];
return r;
}
var useSuggestionInput = function (containerNode) {
var elementHeight = 60;
var _a = React.useState(-1), selectedIndex = _a[0], setSelectedIndex = _a[1];
var _b = React.useState(null), boxTopPosition = _b[0], setBoxTopPosition = _b[1];
var _c = React.useState(null), offset = _c[0], setOffset = _c[1];
var addEventListener = function () {
window.addEventListener('scroll', checkHeight);
};
var removeEventListener = function () {
window.removeEventListener('scroll', checkHeight);
};
var targetNode = React.useRef(containerNode.current);
var increaseListIndex = function (num) {
setSelectedIndex(function (s) { return s + num; });
};
var scrollUp = function (scrollContainer, listLength) {
var tmpContainer = scrollContainer;
if (listLength > 0 && tmpContainer) {
var topOffsetElements = Math.ceil(tmpContainer.scrollTop / elementHeight);
if (selectedIndex === topOffsetElements) {
tmpContainer.scrollTop = (topOffsetElements - 1) * elementHeight;
}
}
};
var scrollDown = function (scrollContainer, listLength) {
var tmpContainer = scrollContainer;
if (listLength > 0 && tmpContainer) {
var containerHeight = tmpContainer.offsetHeight;
var maxFullContainElements = Math.floor(containerHeight / elementHeight);
var splittedElementHeight = containerHeight % elementHeight;
var needToScroll = elementHeight - splittedElementHeight;
if (maxFullContainElements - 1 === selectedIndex) {
tmpContainer.scrollTop = needToScroll;
}
if (maxFullContainElements - 1 < selectedIndex) {
tmpContainer.scrollTop += elementHeight;
}
}
};
var onKeyPress = function (event, listLength, handleSelectElement, scrollContainer) {
switch (event.key) {
case 'ArrowDown': {
scrollDown(scrollContainer, listLength);
if (selectedIndex === -1) {
setSelectedIndex(0);
}
else if (selectedIndex < listLength - 1) {
increaseListIndex(1);
}
break;
}
case 'ArrowUp': {
scrollUp(scrollContainer, listLength);
if (selectedIndex > 0) {
increaseListIndex(-1);
}
break;
}
case 'Enter': {
event.preventDefault();
if (selectedIndex !== -1) {
handleSelectElement(event);
setSelectedIndex(-1);
}
break;
}
default:
break;
}
};
var checkHeight = function () {
var node = targetNode.current;
if (node) {
var rect = node.getBoundingClientRect();
var tmpBoxTopPosition = rect.top + rect.height + 55;
var tmpOffset = rect.top;
if (boxTopPosition !== tmpBoxTopPosition) {
setBoxTopPosition(tmpBoxTopPosition);
}
if (tmpOffset !== offset) {
setOffset(tmpOffset);
}
}
};
return ({
addEventListener: addEventListener,
removeEventListener: removeEventListener,
boxTopPosition: boxTopPosition,
onKeyPress: onKeyPress,
selectedIndex: selectedIndex,
checkHeight: checkHeight,
offset: offset,
});
};
var SearchInput = function (props) {
var inputNode = React.useRef(null);
var inputContainerRef = React.useRef(null);
var scrollContainerRef = React.useRef(null);
var _a = useSuggestionInput(inputContainerRef), boxTopPosition = _a.boxTopPosition, checkHeight = _a.checkHeight, addEventListener = _a.addEventListener, removeEventListener = _a.removeEventListener, selectedIndex = _a.selectedIndex, onKeyPress = _a.onKeyPress;
var _b = React.useState([]), hitList = _b[0], setHitList = _b[1];
var _c = React.useState(false), loadingSuggestions = _c[0], setLoadingSuggestions = _c[1];
var _d = React.useState(false), nothingFound = _d[0], setNothingFound = _d[1];
React.useEffect(function () {
document.addEventListener('mousedown', handleClickOutside);
checkHeight();
return function () {
document.removeEventListener('mousedown', handleClickOutside);
removeEventListener();
};
}, []);
var handleClickOutside = function (event) {
if (inputContainerRef.current) {
var node = inputContainerRef.current;
// @ts-ignore
if (node && !node.contains(event.target)) {
setNothingFound(false);
setHitList([]);
if (props.handleClickOutside) {
props.handleClickOutside(event);
}
}
}
};
var clearAndHide = function () {
if (nothingFound || hitList.length > 0 || (inputNode.current && inputNode.current.value)) {
setNothingFound(false);
setHitList([]);
if (inputNode.current) {
inputNode.current.value = '';
inputNode.current.focus();
}
}
};
var selectClickedElement = function (value) {
props.handleSelectElement(value, clearAndHide);
clearAndHide();
};
var selectCurrentElement = function () {
props.handleSelectElement(hitList[selectedIndex], clearAndHide);
clearAndHide();
};
var searchAction = function (event) { return __awaiter(void 0, void 0, void 0, function () {
var value;
return __generator(this, function (_a) {
value = event.target.value;
if (value.trim().length === 0) {
setNothingFound(true);
setLoadingSuggestions(false);
return [2 /*return*/];
}
if (props.fetchSearchSuggestions) {
setLoadingSuggestions(true);
props.fetchSearchSuggestions(value)
.then(function (list) {
if (list.length === 0) {
setNothingFound(true);
}
else {
setNothingFound(false);
}
setHitList(list);
setLoadingSuggestions(false);
checkHeight();
})
.catch(function () {
setHitList([]);
});
return [2 /*return*/];
}
if (props.suggestionList) {
setNothingFound(false);
setHitList(props.suggestionList);
checkHeight();
}
return [2 /*return*/];
});
}); };
var setNode = function (passedInputNode) {
if (passedInputNode) {
inputNode.current = passedInputNode;
if (props.setInputRef) {
props.setInputRef(passedInputNode);
}
}
};
var onKeyDownItem = function (event) {
if (scrollContainerRef.current) {
onKeyPress(event, hitList.length, selectCurrentElement, scrollContainerRef.current);
}
};
return (React__default.createElement("div", { className: "input_container", ref: inputContainerRef, onKeyDown: onKeyDownItem },
React__default.createElement("div", { className: "search_container" },
React__default.createElement("div", { className: "search_container_input" },
React__default.createElement("input", { ref: setNode, className: props.inputClassName, placeholder: props.inputPlaceholder, onChange: searchAction, onFocus: addEventListener, onBlur: removeEventListener })),
React__default.createElement("div", { className: "results_container", style: { maxHeight: "calc(100vh - " + (boxTopPosition || '100') + "px)" }, ref: scrollContainerRef }, loadingSuggestions ? (React__default.createElement("div", null,
React__default.createElement("div", { className: "nothing_found_container" }, props.loadingSpinner || React__default.createElement("div", null, "loading")))) : (React__default.createElement("div", null, nothingFound
? (React__default.createElement("div", { className: "nothing_found_container" }, props.emptyMessage))
: (React__default.createElement("span", null, hitList.map(function (item, index) { return (props.renderListItem(selectedIndex === index, item, selectClickedElement)); })))))))));
};
SearchInput.defaultProps = {
inputClassName: '',
setInputRef: function () { return null; },
handleClickOutside: function () { return null; },
emptyMessage: 'empty',
inputPlaceholder: 'Search',
};
var CloseIcon = function () { return (React__default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24" },
React__default.createElement("path", { fill: "currentColor",
// tslint:disable-next-line:max-line-length
d: "M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z" }),
React__default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }))); };
var SampleChip = function (_a) {
var value = _a.value;
var removeThisChip = function () { return value.onRemove(value); };
return (React__default.createElement("div", { className: "chip" },
value.name,
React__default.createElement("div", { className: "remove", onClick: removeThisChip },
React__default.createElement(CloseIcon, null))));
};
var SampleListItem = function (_a) {
var value = _a.value, handleSelect = _a.handleSelect, selected = _a.selected, preSelected = _a.preSelected;
var selectItem = function () { return handleSelect(value); };
return (React__default.createElement("div", { className: "list_item " + (preSelected ? 'pre_selected' : '') + " " + (selected ? 'selected' : ''), onClick: selectItem }, value.name));
};
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css = "@keyframes grow-chip {\n 0% {\n transform: scale(0); }\n 60% {\n transform: scale(1.1); }\n 100% {\n transform: scale(1); } }\n\n.chips_input_container {\n display: flex;\n align-items: center;\n padding: 5px;\n min-height: 50px;\n border-radius: 5px;\n border: 1px solid grey;\n background-color: white; }\n\n.chips_wrapper {\n display: flex;\n flex-wrap: wrap;\n flex: 0 100%;\n margin-left: 10px;\n position: relative; }\n .chips_wrapper input {\n font-size: 1rem;\n line-height: 26px;\n letter-spacing: 0.3px;\n outline: none;\n margin-left: 5px;\n background-color: rgba(0, 0, 0, 0); }\n .chips_wrapper input:focus {\n border: none;\n outline: none; }\n .chips_wrapper input::placeholder {\n color: #d9d9d9;\n opacity: 1; }\n\n.results_container {\n position: absolute;\n width: 100%;\n overflow-y: auto;\n z-index: 999;\n box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.19);\n left: 0;\n border-radius: 5px; }\n\n.input_container {\n height: auto;\n align-items: center;\n display: flex;\n flex: 1; }\n\n.search_container {\n width: 100%; }\n .search_container_input {\n display: block;\n align-items: center;\n padding-right: 5px; }\n .search_container_input input {\n width: 100%;\n min-width: 200px;\n border: none; }\n\n.chip {\n padding: 5px 10px 5px 10px;\n margin: 5px;\n text-transform: uppercase;\n display: flex;\n border-radius: 50px;\n background-color: #969696;\n font-size: 12px;\n font-weight: 600;\n line-height: 2.17;\n letter-spacing: 0.2px;\n align-items: center;\n color: white;\n animation: grow-chip 250ms cubic-bezier(0.32, 0.62, 0.2, 0.88);\n cursor: pointer; }\n .chip > div {\n margin-left: 10px;\n height: 24px;\n width: 24px;\n color: white; }\n .chip:hover:after {\n display: block; }\n\n.nothing_found_container {\n display: flex;\n align-items: center;\n justify-content: center;\n border-top: 1px solid white;\n height: 60px;\n color: #d9d9d9;\n background-color: white; }\n\n.list_item {\n background-color: white;\n color: black;\n padding: 10px;\n cursor: pointer; }\n .list_item:hover {\n background-color: grey;\n color: white; }\n .list_item.selected {\n color: white;\n background-color: grey; }\n .list_item.pre_selected {\n background-color: lightgray; }\n";
styleInject(css);
var SearchIcon = function () { return (React__default.createElement("svg", { fill: "grey", xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24" },
React__default.createElement("path", {
// tslint:disable-next-line:max-line-length
d: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" }),
React__default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }))); };
var CustomChips = function (props) {
var inputRef = React.useRef(null);
var _a = React.useState(props.chipsData || []), chipsData = _a[0], setChipsData = _a[1];
React.useEffect(function () {
setChipsData(props.chipsData || []);
}, [props.chipsData]);
var changeChips = function (chips) {
setChipsData(chips);
if (props.onChange) {
props.onChange(chips);
}
};
var onKeyPress = function (event) {
var tmpTarget = event.target;
if (event.key === 'Backspace' && tmpTarget.value === '') {
var tmpChipsData = __spreadArrays(chipsData);
tmpChipsData.pop();
changeChips(tmpChipsData);
}
};
var addItem = function (item) {
if (!chipsData.some(function (chip) { return chip.id === item.id; })) {
var tmpChipsData = __spreadArrays(chipsData, [item]);
changeChips(tmpChipsData);
}
};
var removeChip = function (chip) {
var tmpChipsData = __spreadArrays(chipsData);
var filteredData = tmpChipsData.filter(function (item) { return item.id !== chip.id; });
changeChips(filteredData);
};
var renderChip = function (chip) {
var tmpChip = __assign(__assign({}, chip), { onRemove: removeChip });
if (props.renderChip) {
return props.renderChip(tmpChip);
}
};
var inputSetting = function (input) {
inputRef.current = input;
};
var onKeyDownItem = function (event) {
onKeyPress(event);
};
var onClickItem = function () {
if (inputRef.current) {
inputRef.current.focus();
}
};
var renderListItem = function (selected, value, handleSelect) {
if (props.renderItem) {
props.renderItem(selected, value, handleSelect);
}
return (React__default.createElement(SampleListItem, { preSelected: chipsData.includes(value), value: value, selected: selected, handleSelect: handleSelect }));
};
return (React__default.createElement("div", { onKeyDown: onKeyDownItem, onClick: onClickItem, className: "chips_input_container " + props.chipsWrapperClassName },
!props.noIcon && (React__default.createElement("div", null, props.searchIcon || React__default.createElement(SearchIcon, null))),
React__default.createElement("div", { className: "chips_wrapper" },
chipsData && chipsData.map(function (item) { return (renderChip(item)); }),
React__default.createElement(SearchInput, { loadingSpinner: props.loadingSpinner, fetchSearchSuggestions: props.fetchSearchSuggestions, suggestionList: props.suggestionList, minLength: 1, inputClassName: props.inputClassName || 'chips-input', debounceTimeout: 250, handleSelectElement: addItem, renderListItem: renderListItem, setInputRef: inputSetting, inputPlaceholder: props.inputPlaceholder || 'Search', emptyMessage: props.emptyMessage || 'empty' }))));
};
CustomChips.defaultProps = {
chipsData: [],
suggestionList: [],
emptyMessage: 'empty',
inputPlaceholder: 'Search',
renderChip: function (value) { return (React__default.createElement(SampleChip, { key: value.id, value: value })); },
};
module.exports = CustomChips;