2022-02-17 11:37:44 +00:00
|
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
2022-02-02 13:18:15 +00:00
|
|
|
import PropTypes from "prop-types";
|
|
|
|
import Scrollbar from "../scrollbar";
|
2022-02-21 20:30:42 +00:00
|
|
|
import { useClickOutside } from "../utils/useClickOutside.js";
|
2022-02-02 13:18:15 +00:00
|
|
|
|
|
|
|
import {
|
|
|
|
StyledContent,
|
|
|
|
StyledChipGroup,
|
|
|
|
StyledChipWithInput,
|
2022-03-06 19:35:31 +00:00
|
|
|
} from "./styled-emailchips";
|
2022-03-02 09:55:32 +00:00
|
|
|
import {
|
|
|
|
MAX_EMAIL_LENGTH_WITH_DOTS,
|
|
|
|
sliceEmail,
|
|
|
|
} from "./sub-components/helpers";
|
2022-02-25 17:38:58 +00:00
|
|
|
import InputGroup from "./sub-components/input-group";
|
|
|
|
import ChipsRender from "./sub-components/chips-render";
|
2022-03-06 19:35:31 +00:00
|
|
|
import { EmailSettings, parseAddresses } from "../utils/email";
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-03-02 09:55:32 +00:00
|
|
|
const calcMaxLengthInput = (exceededLimit) =>
|
|
|
|
exceededLimit * MAX_EMAIL_LENGTH_WITH_DOTS;
|
2022-02-28 19:37:10 +00:00
|
|
|
|
2022-03-06 19:35:31 +00:00
|
|
|
const EmailChips = ({
|
2022-02-14 10:48:52 +00:00
|
|
|
options,
|
|
|
|
placeholder,
|
|
|
|
onChange,
|
2022-02-21 20:30:42 +00:00
|
|
|
clearButtonLabel,
|
|
|
|
existEmailText,
|
2022-02-14 10:48:52 +00:00
|
|
|
invalidEmailText,
|
2022-02-25 11:42:31 +00:00
|
|
|
exceededLimit,
|
2022-02-25 17:38:58 +00:00
|
|
|
exceededLimitText,
|
2022-02-28 19:37:10 +00:00
|
|
|
exceededLimitInputText,
|
|
|
|
chipOverLimitText,
|
2022-02-14 10:48:52 +00:00
|
|
|
...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-28 19:37:10 +00:00
|
|
|
const [isExistedOn, setIsExistedOn] = useState(false);
|
|
|
|
const [isExceededLimitChips, setIsExceededLimitChips] = useState(false);
|
|
|
|
const [isExceededLimitInput, setIsExceededLimitInput] = useState(false);
|
|
|
|
|
2022-02-17 11:37:44 +00:00
|
|
|
const containerRef = useRef(null);
|
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-21 20:30:42 +00:00
|
|
|
const chipsCount = useRef(options?.length);
|
2022-02-07 15:49:08 +00:00
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
useEffect(() => {
|
2022-03-06 19:35:31 +00:00
|
|
|
onChange(
|
2022-03-31 12:34:18 +00:00
|
|
|
chips.map((it) => {
|
|
|
|
if (it?.name === it?.email || it?.name === "") {
|
2022-03-06 19:35:31 +00:00
|
|
|
return {
|
|
|
|
email: it?.email,
|
2022-03-31 12:34:18 +00:00
|
|
|
isValid: it?.isValid,
|
2022-03-06 19:35:31 +00:00
|
|
|
};
|
2022-03-31 12:34:18 +00:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
name: it?.name,
|
|
|
|
email: it?.email,
|
|
|
|
isValid: it?.isValid,
|
|
|
|
};
|
|
|
|
})
|
2022-03-06 19:35:31 +00:00
|
|
|
);
|
2022-03-02 19:14:05 +00:00
|
|
|
}, [chips]);
|
2022-02-07 15:49:08 +00:00
|
|
|
|
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-25 17:38:58 +00:00
|
|
|
useClickOutside(
|
|
|
|
blockRef,
|
|
|
|
() => {
|
|
|
|
if (selectedChips.length > 0) {
|
|
|
|
setSelectedChips([]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
selectedChips
|
|
|
|
);
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-02-28 19:37:10 +00:00
|
|
|
useClickOutside(inputRef, () => {
|
|
|
|
onHideAllTooltips();
|
|
|
|
});
|
|
|
|
|
2022-02-21 20:30:42 +00:00
|
|
|
const onClick = (value, isShiftKey) => {
|
|
|
|
if (isShiftKey) {
|
2022-03-06 19:35:31 +00:00
|
|
|
const isExisted = !!selectedChips?.find((it) => it.email === value.email);
|
2022-02-21 20:30:42 +00:00
|
|
|
return isExisted
|
|
|
|
? setSelectedChips(
|
2022-03-06 19:35:31 +00:00
|
|
|
selectedChips.filter((it) => it.email != value.email)
|
2022-02-21 20:30:42 +00:00
|
|
|
)
|
|
|
|
: setSelectedChips([value, ...selectedChips]);
|
|
|
|
} else {
|
|
|
|
setSelectedChips([value]);
|
|
|
|
}
|
|
|
|
};
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-02-21 20:30:42 +00:00
|
|
|
const onDoubleClick = (value) => {
|
|
|
|
setCurrentChip(value);
|
|
|
|
};
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-02-17 11:37:44 +00:00
|
|
|
const onDelete = useCallback(
|
|
|
|
(value) => {
|
2022-03-06 19:35:31 +00:00
|
|
|
setChips(chips.filter((it) => it.email !== value.email));
|
2022-02-17 11:37:44 +00:00
|
|
|
},
|
|
|
|
[chips]
|
|
|
|
);
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-02-07 15:49:08 +00:00
|
|
|
const checkSelected = (value) => {
|
2022-03-06 19:35:31 +00:00
|
|
|
return !!selectedChips?.find((item) => item?.email === value?.email);
|
2022-02-07 15:49:08 +00:00
|
|
|
};
|
|
|
|
|
2022-02-21 20:30:42 +00:00
|
|
|
const onSaveNewChip = (value, newValue) => {
|
2022-03-06 19:35:31 +00:00
|
|
|
const settings = new EmailSettings();
|
|
|
|
settings.allowName = true;
|
|
|
|
let parsed = parseAddresses(newValue, settings);
|
|
|
|
parsed[0].isValid = parsed[0].isValid();
|
|
|
|
if (newValue && newValue !== `"${value?.name}" <${value?.email}>`) {
|
|
|
|
const newChips = chips.map((it) => {
|
|
|
|
return it.email === value.email ? sliceEmail(parsed[0]) : it;
|
|
|
|
});
|
|
|
|
setChips(newChips);
|
|
|
|
setSelectedChips([sliceEmail(parsed[0])]);
|
2022-02-21 20:30:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
containerRef.current.setAttribute("tabindex", "-1");
|
|
|
|
containerRef.current.focus();
|
|
|
|
|
|
|
|
setCurrentChip(null);
|
|
|
|
};
|
2022-02-02 13:18:15 +00:00
|
|
|
|
|
|
|
const copyToClipbord = () => {
|
2022-02-07 15:49:08 +00:00
|
|
|
if (currentChip === null) {
|
|
|
|
navigator.clipboard.writeText(
|
2022-02-22 08:11:40 +00:00
|
|
|
selectedChips
|
|
|
|
.map((it) => {
|
2022-03-06 19:35:31 +00:00
|
|
|
if (it.name !== it.email) {
|
|
|
|
let copyItem = `"${it.name}" <${it.email}>`;
|
2022-02-22 08:11:40 +00:00
|
|
|
return copyItem;
|
|
|
|
} else {
|
2022-03-06 19:35:31 +00:00
|
|
|
return it.email;
|
2022-02-22 08:11:40 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.join(", ")
|
2022-02-07 15:49:08 +00:00
|
|
|
);
|
|
|
|
}
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const onKeyDown = (e) => {
|
2022-02-21 20:30:42 +00:00
|
|
|
const whiteList = [
|
|
|
|
"Enter",
|
|
|
|
"Escape",
|
|
|
|
"Backspace",
|
|
|
|
"Delete",
|
|
|
|
"ArrowRigth",
|
|
|
|
"ArrowLeft",
|
|
|
|
"ArrowLeft",
|
|
|
|
"ArrowRight",
|
|
|
|
"KeyC",
|
|
|
|
];
|
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
const code = e.code;
|
2022-02-18 10:27:07 +00:00
|
|
|
|
2022-02-17 11:37:44 +00:00
|
|
|
const isShiftDown = e.shiftKey;
|
|
|
|
const isCtrlDown = e.ctrlKey;
|
2022-02-02 13:18:15 +00:00
|
|
|
|
2022-02-21 20:30:42 +00:00
|
|
|
if (!whiteList.includes(code) && !isCtrlDown && !isShiftDown) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (code === "Enter" && selectedChips.length == 1 && !currentChip) {
|
2022-02-18 10:27:07 +00:00
|
|
|
e.stopPropagation();
|
2022-02-21 20:30:42 +00:00
|
|
|
setCurrentChip(selectedChips[0]);
|
2022-02-18 10:27:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-02 13:18:15 +00:00
|
|
|
if (code === "Escape") {
|
2022-02-21 20:30:42 +00:00
|
|
|
setSelectedChips(currentChip ? [currentChip] : []);
|
|
|
|
containerRef.current.setAttribute("tabindex", "0");
|
|
|
|
containerRef.current.focus();
|
2022-02-02 13:18:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-18 10:27:07 +00:00
|
|
|
if (
|
|
|
|
selectedChips.length > 0 &&
|
|
|
|
(code === "Backspace" || code === "Delete") &&
|
|
|
|
!currentChip
|
|
|
|
) {
|
2022-02-08 13:41:29 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-02-21 20:30:42 +00:00
|
|
|
if (selectedChips.length > 0 && !currentChip) {
|
|
|
|
let chip = null;
|
2022-02-02 13:18:15 +00:00
|
|
|
|
|
|
|
if (isShiftDown && code === "ArrowRigth") {
|
|
|
|
chip = selectedChips[selectedChips.length - 1];
|
|
|
|
} else {
|
|
|
|
chip = selectedChips[0];
|
|
|
|
}
|
|
|
|
|
2022-03-06 19:35:31 +00:00
|
|
|
const index = chips.findIndex((it) => it.email === chip?.email);
|
2022-02-21 20:30:42 +00:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-02-28 19:37:10 +00:00
|
|
|
const goFromInputToChips = () => {
|
|
|
|
setSelectedChips([chips[chips?.length - 1]]);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onClearClick = () => {
|
|
|
|
setChips([]);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onHideAllTooltips = () => {
|
|
|
|
setIsExceededLimitChips(false);
|
|
|
|
setIsExistedOn(false);
|
|
|
|
setIsExceededLimitInput(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
const showTooltipOfLimit = () => {
|
|
|
|
setIsExceededLimitInput(true);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onAddChip = (chipsToAdd) => {
|
|
|
|
setIsExceededLimitChips(chips.length >= exceededLimit);
|
|
|
|
if (chips.length >= exceededLimit) return;
|
2022-03-06 19:35:31 +00:00
|
|
|
const filterLimit = exceededLimit - chips.length;
|
2022-02-28 19:37:10 +00:00
|
|
|
|
2022-03-06 19:35:31 +00:00
|
|
|
const filteredChips = chipsToAdd.map(sliceEmail).filter((it, index) => {
|
2022-03-02 09:55:32 +00:00
|
|
|
const isExisted = !!chips.find(
|
2022-03-06 19:35:31 +00:00
|
|
|
(chip) => chip.email === it || chip.email === it?.email
|
2022-03-02 09:55:32 +00:00
|
|
|
);
|
|
|
|
if (chipsToAdd.length === 1) {
|
|
|
|
setIsExistedOn(isExisted);
|
|
|
|
if (isExisted) return false;
|
|
|
|
}
|
2022-03-06 19:35:31 +00:00
|
|
|
return !isExisted && index < filterLimit;
|
2022-03-02 09:55:32 +00:00
|
|
|
});
|
2022-02-28 19:37:10 +00:00
|
|
|
setChips([...chips, ...filteredChips]);
|
|
|
|
};
|
|
|
|
|
2022-02-25 17:38:58 +00:00
|
|
|
return (
|
|
|
|
<StyledContent {...props}>
|
|
|
|
<StyledChipGroup onKeyDown={onKeyDown} ref={containerRef} tabindex="-1">
|
|
|
|
<StyledChipWithInput length={chips.length}>
|
|
|
|
<Scrollbar scrollclass={"scroll"} stype="thumbV" ref={scrollbarRef}>
|
|
|
|
<ChipsRender
|
|
|
|
chips={chips}
|
|
|
|
checkSelected={checkSelected}
|
2022-02-21 20:30:42 +00:00
|
|
|
currentChip={currentChip}
|
2022-02-25 17:38:58 +00:00
|
|
|
blockRef={blockRef}
|
|
|
|
onClick={onClick}
|
2022-02-21 20:30:42 +00:00
|
|
|
invalidEmailText={invalidEmailText}
|
2022-03-01 11:53:57 +00:00
|
|
|
chipOverLimitText={chipOverLimitText}
|
2022-02-21 20:30:42 +00:00
|
|
|
onDelete={onDelete}
|
|
|
|
onDoubleClick={onDoubleClick}
|
|
|
|
onSaveNewChip={onSaveNewChip}
|
|
|
|
/>
|
2022-02-08 13:41:29 +00:00
|
|
|
</Scrollbar>
|
2022-02-25 17:38:58 +00:00
|
|
|
|
|
|
|
<InputGroup
|
|
|
|
placeholder={placeholder}
|
|
|
|
exceededLimitText={exceededLimitText}
|
|
|
|
existEmailText={existEmailText}
|
2022-02-28 19:37:10 +00:00
|
|
|
exceededLimitInputText={exceededLimitInputText}
|
|
|
|
clearButtonLabel={clearButtonLabel}
|
|
|
|
inputRef={inputRef}
|
|
|
|
containerRef={containerRef}
|
|
|
|
maxLength={calcMaxLengthInput(exceededLimit)}
|
|
|
|
goFromInputToChips={goFromInputToChips}
|
|
|
|
onClearClick={onClearClick}
|
|
|
|
isExistedOn={isExistedOn}
|
|
|
|
isExceededLimitChips={isExceededLimitChips}
|
|
|
|
isExceededLimitInput={isExceededLimitInput}
|
|
|
|
onHideAllTooltips={onHideAllTooltips}
|
|
|
|
showTooltipOfLimit={showTooltipOfLimit}
|
|
|
|
onAddChip={onAddChip}
|
2022-02-25 17:38:58 +00:00
|
|
|
/>
|
2022-02-08 13:41:29 +00:00
|
|
|
</StyledChipWithInput>
|
2022-02-02 13:18:15 +00:00
|
|
|
</StyledChipGroup>
|
|
|
|
</StyledContent>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-03-06 19:35:31 +00:00
|
|
|
EmailChips.propTypes = {
|
2022-02-03 11:57:50 +00:00
|
|
|
/** Array of objects with chips */
|
2022-02-17 11:37:44 +00:00
|
|
|
options: PropTypes.arrayOf(PropTypes.object),
|
|
|
|
/** Placeholder text for the input */
|
2022-02-02 13:18:15 +00:00
|
|
|
placeholder: PropTypes.string,
|
2022-02-21 20:30:42 +00:00
|
|
|
/** The text that is displayed in the button for cleaning all chips */
|
|
|
|
clearButtonLabel: PropTypes.string,
|
2022-02-17 11:37:44 +00:00
|
|
|
/** Warning text when entering an existing email */
|
2022-02-14 10:48:52 +00:00
|
|
|
existEmailText: PropTypes.string,
|
2022-02-17 11:37:44 +00:00
|
|
|
/** Warning text when entering an invalid email */
|
2022-02-14 10:48:52 +00:00
|
|
|
invalidEmailText: PropTypes.string,
|
2022-02-25 17:38:58 +00:00
|
|
|
/** Limit of chips */
|
|
|
|
exceededLimit: PropTypes.number,
|
2022-02-25 11:42:31 +00:00
|
|
|
/** Warning text when entering the number of chips exceeding the limit */
|
2022-02-25 17:38:58 +00:00
|
|
|
exceededLimitText: PropTypes.string,
|
2022-02-28 19:37:10 +00:00
|
|
|
/** Warning text when entering the number of characters in input exceeding the limit */
|
|
|
|
exceededLimitInputText: PropTypes.string,
|
|
|
|
/** Warning text when entering the number of email characters exceeding the limit */
|
|
|
|
chipOverLimitText: PropTypes.string,
|
2022-02-17 11:37:44 +00:00
|
|
|
/** Will be called when the selected items are changed */
|
|
|
|
onChange: PropTypes.func.isRequired,
|
2022-02-02 13:18:15 +00:00
|
|
|
};
|
|
|
|
|
2022-03-06 19:35:31 +00:00
|
|
|
EmailChips.defaultProps = {
|
2022-02-17 11:37:44 +00:00
|
|
|
placeholder: "Invite people by name or email",
|
2022-02-25 17:38:58 +00:00
|
|
|
clearButtonLabel: "Clear list",
|
|
|
|
existEmailText: "This email address has already been entered",
|
|
|
|
invalidEmailText: "Invalid email address",
|
|
|
|
exceededLimitText:
|
|
|
|
"The limit on the number of emails has reached the maximum",
|
2022-02-28 19:37:10 +00:00
|
|
|
exceededLimitInputText:
|
|
|
|
"The limit on the number of characters has reached the maximum value",
|
2022-02-25 17:38:58 +00:00
|
|
|
exceededLimit: 50,
|
2022-02-03 11:57:50 +00:00
|
|
|
};
|
|
|
|
|
2022-03-06 19:35:31 +00:00
|
|
|
export default EmailChips;
|