2022-02-07 15:49:08 +00:00
|
|
|
import React, { useEffect, useRef, useState } from "react";
|
2022-02-02 13:18:15 +00:00
|
|
|
import PropTypes from "prop-types";
|
|
|
|
|
|
|
|
import Chip from "./sub-components/chip";
|
|
|
|
import TextInput from "../text-input";
|
|
|
|
import Scrollbar from "../scrollbar";
|
|
|
|
import { EmailSettings, parseAddress } from "../utils/email/";
|
2022-02-11 14:43:50 +00:00
|
|
|
import { useClickOutside } from "./sub-components/use-click-outside";
|
|
|
|
import Link from "../link";
|
2022-02-02 13:18:15 +00:00
|
|
|
|
|
|
|
import {
|
|
|
|
StyledContent,
|
|
|
|
StyledChipGroup,
|
|
|
|
StyledChipWithInput,
|
2022-02-08 13:41:29 +00:00
|
|
|
StyledAllChips,
|
|
|
|
StyledInputWithLink,
|
2022-02-11 14:43:50 +00:00
|
|
|
StyledTooltip,
|
2022-02-02 13:18:15 +00:00
|
|
|
} from "./styled-inputwithchips";
|
|
|
|
|
2022-02-14 10:48:52 +00:00
|
|
|
const InputWithChips = ({
|
|
|
|
options,
|
|
|
|
placeholder,
|
|
|
|
onChange,
|
|
|
|
existEmailText = "This email address has already been entered",
|
|
|
|
invalidEmailText,
|
|
|
|
...props
|
|
|
|
}) => {
|
2022-02-07 15:49:08 +00:00
|
|
|
const [chips, setChips] = useState(options || []);
|
2022-02-02 13:18:15 +00:00
|
|
|
const [currentChip, setCurrentChip] = useState(null);
|
|
|
|
const [selectedChips, setSelectedChips] = useState([]);
|
|
|
|
|
2022-02-07 15:49:08 +00:00
|
|
|
const [value, setValue] = useState("");
|
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
const [isShiftDown, setIsShiftDown] = useState(false);
|
|
|
|
const [isCtrlDown, setIsCtrlDown] = useState(false);
|
|
|
|
|
2022-02-11 14:43:50 +00:00
|
|
|
const [isExistedOn, setIsExistedOn] = useState(false);
|
2022-02-07 15:49:08 +00:00
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
const emailSettings = new EmailSettings();
|
|
|
|
|
2022-02-07 15:49:08 +00:00
|
|
|
const inputRef = useRef(null);
|
|
|
|
const blockRef = useRef(null);
|
2022-02-11 14:43:50 +00:00
|
|
|
const scrollbarRef = useRef(null);
|
2022-02-15 11:33:15 +00:00
|
|
|
const chipsCount = useRef(options.length);
|
2022-02-07 15:49:08 +00:00
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
useEffect(() => {
|
|
|
|
document.addEventListener("keydown", onKeyDown);
|
|
|
|
document.addEventListener("keyup", onKeyUp);
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener("keydown", onKeyDown);
|
|
|
|
document.removeEventListener("keyup", onKeyUp);
|
|
|
|
};
|
2022-02-08 13:41:29 +00:00
|
|
|
}, [selectedChips, currentChip, isShiftDown, isCtrlDown]);
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-02-07 15:49:08 +00:00
|
|
|
useEffect(() => {
|
|
|
|
onChange(selectedChips);
|
|
|
|
}, [selectedChips]);
|
|
|
|
|
2022-02-15 11:33:15 +00:00
|
|
|
useEffect(() => {
|
|
|
|
const isChipAdd = chips.length > chipsCount.current;
|
|
|
|
if (scrollbarRef.current && isChipAdd) {
|
|
|
|
scrollbarRef.current.scrollToBottom();
|
|
|
|
}
|
|
|
|
chipsCount.current = chips.length;
|
|
|
|
}, [chips.length]);
|
|
|
|
|
2022-02-11 14:43:50 +00:00
|
|
|
useClickOutside(blockRef, () => {
|
2022-02-15 07:42:37 +00:00
|
|
|
setSelectedChips([]);
|
2022-02-11 14:43:50 +00:00
|
|
|
setIsExistedOn(false);
|
|
|
|
});
|
2022-02-07 15:49:08 +00:00
|
|
|
|
|
|
|
const onInputChange = (e) => {
|
2022-02-02 13:18:15 +00:00
|
|
|
setValue(e.target.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onClick = (value) => {
|
2022-02-03 11:57:50 +00:00
|
|
|
if (isShiftDown) {
|
2022-02-07 15:49:08 +00:00
|
|
|
const isExisted = !!selectedChips?.find((it) => it.value === value.value);
|
2022-02-03 11:57:50 +00:00
|
|
|
return isExisted
|
|
|
|
? setSelectedChips(
|
|
|
|
selectedChips.filter((it) => it.value != value.value)
|
|
|
|
)
|
|
|
|
: setSelectedChips([value, ...selectedChips]);
|
|
|
|
} else {
|
|
|
|
setSelectedChips([value]);
|
|
|
|
}
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const onDoubleClick = (value) => {
|
2022-02-11 14:43:50 +00:00
|
|
|
setSelectedChips([]);
|
2022-02-02 13:18:15 +00:00
|
|
|
setCurrentChip(value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDelete = (value) => {
|
2022-02-07 15:49:08 +00:00
|
|
|
setChips(chips.filter((it) => it.value !== value.value));
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const onEnterPress = () => {
|
2022-02-07 15:49:08 +00:00
|
|
|
if (value.trim().length > 0) {
|
2022-02-08 13:58:29 +00:00
|
|
|
const separators = [",", " ", ", "];
|
2022-02-07 15:49:08 +00:00
|
|
|
const chipsFromString = value
|
2022-02-08 13:58:29 +00:00
|
|
|
.split(new RegExp(separators.join("|"), "g"))
|
2022-02-11 14:43:50 +00:00
|
|
|
.filter((it) => it.trim().length !== 0);
|
|
|
|
|
|
|
|
if (chipsFromString.length === 1) {
|
|
|
|
let isExisted = !!chips.find(
|
|
|
|
(chip) => chip.value === chipsFromString[0]
|
|
|
|
);
|
|
|
|
setIsExistedOn(isExisted);
|
|
|
|
if (isExisted) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const filteredChips = chipsFromString
|
2022-02-07 15:49:08 +00:00
|
|
|
.filter((it) => {
|
2022-02-11 14:43:50 +00:00
|
|
|
return !chips.find((chip) => chip.value === it);
|
2022-02-07 15:49:08 +00:00
|
|
|
})
|
|
|
|
.map((it) => ({ label: it, value: it }));
|
|
|
|
|
2022-02-11 14:43:50 +00:00
|
|
|
setChips([...chips, ...filteredChips]);
|
2022-02-02 13:18:15 +00:00
|
|
|
setValue("");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const checkEmail = (email) => {
|
|
|
|
const emailObj = parseAddress(email, emailSettings);
|
|
|
|
return emailObj.isValid();
|
|
|
|
};
|
|
|
|
|
2022-02-07 15:49:08 +00:00
|
|
|
const checkSelected = (value) => {
|
|
|
|
return !!selectedChips?.find((item) => item?.value === value?.value);
|
|
|
|
};
|
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
const onSaveNewChip = (value, newValue) => {
|
2022-02-07 15:49:08 +00:00
|
|
|
if (newValue && newValue !== value.value) {
|
|
|
|
setChips(
|
|
|
|
chips.map((it) =>
|
|
|
|
it.value === value.value ? { label: newValue, value: newValue } : it
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const onInputKeyDown = (e) => {
|
2022-02-07 15:49:08 +00:00
|
|
|
e.stopPropagation();
|
2022-02-02 13:18:15 +00:00
|
|
|
const code = e.code;
|
2022-02-15 07:42:37 +00:00
|
|
|
const isCursorStart = inputRef.current.selectionStart === 0;
|
2022-02-02 13:18:15 +00:00
|
|
|
if (code === "Enter" || code === "NumpadEnter") onEnterPress();
|
2022-02-15 07:42:37 +00:00
|
|
|
if (code === "ArrowLeft" && isCursorStart) {
|
2022-02-07 15:49:08 +00:00
|
|
|
setSelectedChips([chips[chips.length - 1]]);
|
|
|
|
if (inputRef) {
|
|
|
|
inputRef.current.blur();
|
|
|
|
}
|
|
|
|
}
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const copyToClipbord = () => {
|
2022-02-07 15:49:08 +00:00
|
|
|
if (currentChip === null) {
|
|
|
|
navigator.clipboard.writeText(
|
|
|
|
selectedChips.map((it) => it.value).join(", ")
|
|
|
|
);
|
|
|
|
}
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
2022-02-08 13:41:29 +00:00
|
|
|
const onClearList = () => {
|
|
|
|
setChips([]);
|
|
|
|
};
|
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
const onKeyUp = (e) => {
|
|
|
|
const code = e.code;
|
|
|
|
switch (code) {
|
|
|
|
case "ShiftLeft": {
|
|
|
|
setIsShiftDown(false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "ControlLeft": {
|
|
|
|
setIsCtrlDown(false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const onKeyDown = (e) => {
|
|
|
|
const code = e.code;
|
|
|
|
|
|
|
|
if (code === "ShiftLeft") {
|
|
|
|
setIsShiftDown(true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (code === "ControlLeft") {
|
|
|
|
setIsCtrlDown(true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (code === "Escape") {
|
|
|
|
setSelectedChips([]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-08 13:41:29 +00:00
|
|
|
if (selectedChips.length > 0 && code === "Backspace" && !currentChip) {
|
|
|
|
const filteredChips = chips.filter((e) => !~selectedChips.indexOf(e));
|
|
|
|
setChips(filteredChips);
|
2022-02-02 13:18:15 +00:00
|
|
|
setSelectedChips([]);
|
2022-02-15 10:18:59 +00:00
|
|
|
inputRef.current.focus();
|
2022-02-02 13:18:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selectedChips.length > 0) {
|
|
|
|
let chip = "";
|
|
|
|
|
|
|
|
if (isShiftDown && code === "ArrowRigth") {
|
|
|
|
chip = selectedChips[selectedChips.length - 1];
|
|
|
|
} else {
|
|
|
|
chip = selectedChips[0];
|
|
|
|
}
|
|
|
|
|
2022-02-07 15:49:08 +00:00
|
|
|
const index = chips.findIndex((it) => it === chip);
|
2022-02-02 13:18:15 +00:00
|
|
|
switch (code) {
|
|
|
|
case "ArrowLeft": {
|
|
|
|
if (isShiftDown) {
|
2022-02-07 15:49:08 +00:00
|
|
|
selectedChips.includes(chips[index - 1])
|
2022-02-02 13:18:15 +00:00
|
|
|
? setSelectedChips(
|
2022-02-07 15:49:08 +00:00
|
|
|
selectedChips.filter((it) => it !== chips[index])
|
2022-02-02 13:18:15 +00:00
|
|
|
)
|
2022-02-07 15:49:08 +00:00
|
|
|
: chips[index - 1] &&
|
|
|
|
setSelectedChips([chips[index - 1], ...selectedChips]);
|
|
|
|
} else if (index != 0) {
|
|
|
|
setSelectedChips([chips[index - 1]]);
|
2022-02-02 13:18:15 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "ArrowRight": {
|
|
|
|
if (isShiftDown) {
|
2022-02-07 15:49:08 +00:00
|
|
|
selectedChips.includes(chips[index + 1])
|
2022-02-02 13:18:15 +00:00
|
|
|
? setSelectedChips(
|
2022-02-07 15:49:08 +00:00
|
|
|
selectedChips.filter((it) => it !== chips[index])
|
2022-02-02 13:18:15 +00:00
|
|
|
)
|
2022-02-07 15:49:08 +00:00
|
|
|
: chips[index + 1] &&
|
|
|
|
setSelectedChips([chips[index + 1], ...selectedChips]);
|
2022-02-02 13:18:15 +00:00
|
|
|
} else {
|
2022-02-07 15:49:08 +00:00
|
|
|
if (index != chips.length - 1) {
|
|
|
|
setSelectedChips([chips[index + 1]]);
|
|
|
|
} else {
|
|
|
|
setSelectedChips([]);
|
|
|
|
if (inputRef) {
|
|
|
|
inputRef.current.focus();
|
|
|
|
}
|
|
|
|
}
|
2022-02-02 13:18:15 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "KeyC": {
|
|
|
|
if (isCtrlDown) {
|
|
|
|
copyToClipbord();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2022-02-15 07:42:37 +00:00
|
|
|
<StyledContent>
|
2022-02-02 13:18:15 +00:00
|
|
|
<StyledChipGroup>
|
2022-02-08 13:41:29 +00:00
|
|
|
<StyledChipWithInput length={chips.length}>
|
2022-02-11 14:43:50 +00:00
|
|
|
<Scrollbar scrollclass={"scroll"} stype="thumbV" ref={scrollbarRef}>
|
2022-02-15 07:42:37 +00:00
|
|
|
<StyledAllChips ref={blockRef}>
|
2022-02-11 14:43:50 +00:00
|
|
|
{chips?.map((it) => {
|
2022-02-08 13:41:29 +00:00
|
|
|
return (
|
|
|
|
<Chip
|
|
|
|
key={it?.value}
|
|
|
|
value={it}
|
|
|
|
currentChip={currentChip}
|
|
|
|
isSelected={checkSelected(it)}
|
|
|
|
isValid={checkEmail(it?.value)}
|
|
|
|
onDelete={onDelete}
|
|
|
|
onDoubleClick={onDoubleClick}
|
|
|
|
onSaveNewChip={onSaveNewChip}
|
|
|
|
onClick={onClick}
|
2022-02-14 10:48:52 +00:00
|
|
|
invalidEmailText={invalidEmailText}
|
2022-02-08 13:41:29 +00:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</StyledAllChips>
|
|
|
|
</Scrollbar>
|
|
|
|
<StyledInputWithLink>
|
2022-02-14 10:48:52 +00:00
|
|
|
{isExistedOn && <StyledTooltip>{existEmailText}</StyledTooltip>}
|
2022-02-02 13:18:15 +00:00
|
|
|
<TextInput
|
|
|
|
value={value}
|
2022-02-07 15:49:08 +00:00
|
|
|
onChange={onInputChange}
|
|
|
|
forwardedRef={inputRef}
|
2022-02-02 13:18:15 +00:00
|
|
|
onKeyDown={onInputKeyDown}
|
2022-02-08 13:41:29 +00:00
|
|
|
placeholder={placeholder}
|
2022-02-02 13:18:15 +00:00
|
|
|
withBorder={false}
|
2022-02-14 10:48:52 +00:00
|
|
|
className="textInput"
|
|
|
|
chips={chips.length}
|
2022-02-02 13:18:15 +00:00
|
|
|
/>
|
2022-02-08 13:41:29 +00:00
|
|
|
<Link
|
|
|
|
type="action"
|
|
|
|
isHovered={true}
|
2022-02-14 10:48:52 +00:00
|
|
|
className="link"
|
2022-02-08 13:41:29 +00:00
|
|
|
onClick={onClearList}
|
|
|
|
>
|
|
|
|
Clear list
|
|
|
|
</Link>
|
|
|
|
</StyledInputWithLink>
|
|
|
|
</StyledChipWithInput>
|
2022-02-02 13:18:15 +00:00
|
|
|
</StyledChipGroup>
|
|
|
|
</StyledContent>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
InputWithChips.propTypes = {
|
2022-02-03 11:57:50 +00:00
|
|
|
/** Array of objects with chips */
|
|
|
|
options: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
|
|
/** The placeholder is displayed only when the input is empty */
|
2022-02-02 13:18:15 +00:00
|
|
|
placeholder: PropTypes.string,
|
2022-02-14 10:48:52 +00:00
|
|
|
existEmailText: PropTypes.string,
|
|
|
|
invalidEmailText: PropTypes.string,
|
2022-02-07 15:49:08 +00:00
|
|
|
onChange: PropTypes.func,
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
2022-02-03 11:57:50 +00:00
|
|
|
InputWithChips.defaultProps = {
|
|
|
|
placeholder: "Add placeholder to props",
|
|
|
|
};
|
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
export default InputWithChips;
|