@material-ui/core#ClickAwayListener TypeScript Examples
The following examples show how to use
@material-ui/core#ClickAwayListener.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: TooltipIconCircle.tsx From anchor-web-app with Apache License 2.0 | 6 votes |
export function TouchTooltip({
children,
title,
className,
style,
placement = 'top',
...tooltipProps
}: TooltipIconCircleProps) {
const [open, setOpen] = useState<boolean>(false);
const tooltipOpen = useCallback(() => {
setOpen(true);
}, []);
const tooltipClose = useCallback(() => {
setOpen(false);
}, []);
return (
<ClickAwayListener onClickAway={tooltipClose}>
<IconCircle style={style} className={className} onClick={tooltipOpen}>
<Tooltip
{...tooltipProps}
open={open}
onClose={tooltipClose}
disableFocusListener
disableHoverListener
disableTouchListener
title={title}
placement={placement}
>
<span>{children}</span>
</Tooltip>
</IconCircle>
</ClickAwayListener>
);
}
Example #2
Source File: TooltipLabel.tsx From anchor-web-app with Apache License 2.0 | 6 votes |
export function TouchTooltip({
children,
title,
style,
className,
placement = 'top',
...tooltipProps
}: TooltipLabelProps) {
const [open, setOpen] = useState<boolean>(false);
const tooltipOpen = useCallback(() => {
setOpen(true);
}, []);
const tooltipClose = useCallback(() => {
setOpen(false);
}, []);
return (
<ClickAwayListener onClickAway={tooltipClose}>
<Label style={style} className={className} onClick={tooltipOpen}>
<Tooltip
{...tooltipProps}
open={open}
onClose={tooltipClose}
disableFocusListener
disableHoverListener
disableTouchListener
title={title}
placement={placement}
>
<span>{children}</span>
</Tooltip>
</Label>
</ClickAwayListener>
);
}
Example #3
Source File: ChainSelector.tsx From anchor-web-app with Apache License 2.0 | 6 votes |
ChainSelectorBase = (props: UIElementProps) => {
const { className } = props;
const [open, setOpen] = useState(false);
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<div className={className}>
<ChainButton onClick={() => setOpen((v) => !v)} />
{open && (
<DropdownContainer>
<DropdownBox>
<ChainList onClose={() => setOpen((v) => !v)} />
</DropdownBox>
</DropdownContainer>
)}
</div>
</ClickAwayListener>
);
}
Example #4
Source File: DesktopNotification.tsx From anchor-web-app with Apache License 2.0 | 6 votes |
function DesktopNotificationBase({ className }: DesktopNotificationProps) {
const { terraWalletAddress } = useAccount();
const { permission } = useNotification();
const { liquidationAlert } = useJobs();
const [openDropdown, setOpenDropdown] = useState<boolean>(false);
const toggleOpen = useCallback(() => {
setOpenDropdown((prev) => !prev);
}, []);
const onClickAway = useCallback(() => {
setOpenDropdown(false);
}, []);
const visible = useMemo(() => {
return terraWalletAddress && permission === 'granted';
}, [permission, terraWalletAddress]);
return visible ? (
<ClickAwayListener onClickAway={onClickAway}>
<div className={className} data-enabled={liquidationAlert.enabled}>
<div onClick={toggleOpen} className="notification-icon">
{liquidationAlert.enabled ? <NotificationOn /> : <NotificationOff />}
</div>
{openDropdown && (
<DropdownContainer>
<DropdownBox className="notification-dropdown-box">
<NotificationContent />
</DropdownBox>
</DropdownContainer>
)}
</div>
</ClickAwayListener>
) : null;
}
Example #5
Source File: InfoTooltip.tsx From anchor-web-app with Apache License 2.0 | 6 votes |
export function TouchTooltip({
children,
placement = 'top',
...tooltipProps
}: InfoTooltipProps) {
const [open, setOpen] = useState<boolean>(false);
const tooltipOpen = useCallback(() => {
setOpen(true);
}, []);
const tooltipClose = useCallback(() => {
setOpen(false);
}, []);
return (
<ClickAwayListener onClickAway={tooltipClose}>
<sup onClick={tooltipOpen}>
<Tooltip
{...tooltipProps}
open={open}
onClose={tooltipClose}
disableFocusListener
disableHoverListener
disableTouchListener
title={children}
placement={placement}
>
<InfoOutlined />
</Tooltip>
</sup>
</ClickAwayListener>
);
}
Example #6
Source File: WalletSelector.tsx From anchor-web-app with Apache License 2.0 | 5 votes |
function WalletSelectorBase(props: WalletSelectorProps) {
const { walletAddress, initializing, className, onClick, onClose, children } =
props;
const { uUST } = useBalances();
// ---------------------------------------------
// presentation
// ---------------------------------------------
if (initializing) {
return (
<div className={className}>
<ConnectWalletButton initializing={true} onClick={onClick} />
</div>
);
}
if (!walletAddress) {
return (
<ClickAwayListener onClickAway={onClose}>
<div className={className}>
<ConnectWalletButton onClick={onClick} />
{children}
</div>
</ClickAwayListener>
);
}
return (
<ClickAwayListener onClickAway={onClose}>
<div className={className}>
<ConnectWalletButton
walletAddress={walletAddress}
totalUST={uUST}
onClick={onClick}
/>
{children}
</div>
</ClickAwayListener>
);
}
Example #7
Source File: TransactionWidget.tsx From anchor-web-app with Apache License 2.0 | 5 votes |
TransactionWidgetBase = (props: UIElementProps & { color?: string }) => {
const theme = useTheme();
const { className, color = theme.header.textColor } = props;
const [open, setOpen] = useState(false);
const { backgroundTransactions } = useBackgroundTransactions();
const {
target: { chain },
} = useDeploymentTarget();
const navigate = useNavigate();
const restoreTx = useCallback(() => {
setOpen(false);
navigate('/bridge/restore');
}, [navigate, setOpen]);
if (backgroundTransactions.length === 0 || chain === Chain.Terra) {
return null;
}
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<div className={className}>
<TransactionButton
color={color}
backgroundTransactions={backgroundTransactions}
onClick={() => setOpen((v) => !v)}
closeWidget={() => setOpen(false)}
/>
{open && (
<DropdownContainer className="transaction-dropdown">
<DropdownBox>
<TransactionList
backgroundTransactions={backgroundTransactions}
onClose={() => setOpen((v) => !v)}
footer={
<div className="restore-tx">
<div>Having transaction issues?</div>
<BorderButton onClick={restoreTx}>
Restore transaction
</BorderButton>
</div>
}
/>
</DropdownBox>
</DropdownContainer>
)}
</div>
</ClickAwayListener>
);
}
Example #8
Source File: SQFormDateTimePicker.tsx From SQForm with MIT License | 5 votes |
function SQFormDateTimePicker({
name,
label,
size = 'auto',
isDisabled = false,
placeholder = '',
onBlur,
onChange,
muiFieldProps = {},
}: SQFormDateTimePickerProps): JSX.Element {
const {
formikField: {field, helpers},
fieldState: {isFieldError, isFieldRequired},
fieldHelpers: {handleBlur, HelperTextComponent},
} = useForm<Moment | null, unknown>({
name,
onBlur,
});
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
const handleClose = () => setIsCalendarOpen(false);
const toggleCalendar = () => setIsCalendarOpen(!isCalendarOpen);
const handleClickAway = () => {
if (isCalendarOpen) {
setIsCalendarOpen(false);
}
};
const classes = useStyles();
// An empty string will not reset the DatePicker so we have to pass null
const value: ParsableDate<Moment> | null =
(field.value as ParsableDate<Moment>) ?? null;
const handleChange = (date: Moment | null): void => {
helpers.setValue(date);
onChange && onChange(date);
};
return (
<ClickAwayListener onClickAway={handleClickAway}>
<Grid item sm={size}>
<DateTimePicker
label={label}
disabled={isDisabled}
value={value}
onChange={handleChange}
onClose={handleClose}
onOpen={toggleCalendar}
open={isCalendarOpen}
renderInput={(inputProps) => {
return (
<TextField
{...inputProps}
name={name}
color="primary"
disabled={isDisabled}
error={isFieldError}
fullWidth={true}
InputLabelProps={{shrink: true}}
FormHelperTextProps={{error: isFieldError}}
helperText={!isDisabled && HelperTextComponent}
placeholder={placeholder}
onBlur={handleBlur}
onClick={handleClickAway}
required={isFieldRequired}
classes={classes}
/>
);
}}
{...muiFieldProps}
/>
</Grid>
</ClickAwayListener>
);
}
Example #9
Source File: index.tsx From multisig-react with MIT License | 5 votes |
EllipsisTransactionDetails = ({
address,
knownAddress,
sendModalOpenHandler,
}: EllipsisTransactionDetailsProps): React.ReactElement => {
const classes = useStyles()
const [anchorEl, setAnchorEl] = React.useState(null)
const dispatch = useDispatch()
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
const handleClick = (event) => setAnchorEl(event.currentTarget)
const closeMenuHandler = () => setAnchorEl(null)
const addOrEditEntryHandler = () => {
dispatch(push(`${SAFELIST_ADDRESS}/${currentSafeAddress}/address-book?entryAddress=${address}`))
closeMenuHandler()
}
return (
<ClickAwayListener onClickAway={closeMenuHandler}>
<div className={classes.container} role="menu" tabIndex={0}>
<MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
{sendModalOpenHandler
? [
<MenuItem key="send-again-button" onClick={sendModalOpenHandler}>
Send Again
</MenuItem>,
<Divider key="divider" />,
]
: null}
{knownAddress ? (
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
) : (
<MenuItem onClick={addOrEditEntryHandler}>Add to address book</MenuItem>
)}
</Menu>
</div>
</ClickAwayListener>
)
}
Example #10
Source File: Menu.tsx From glific-frontend with GNU Affero General Public License v3.0 | 5 votes |
Menu: React.SFC<MenuProps> = ({
menus,
children,
eventType = 'Click',
placement = 'top',
}) => {
const [open, setOpen] = useState(false);
const anchorRef = useRef<HTMLDivElement>(null);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const menuList = menus.map((menu: any) => (
<div key={menu.title}>
<MenuItem
onClickHandler={() => {
if (menu.onClick) {
menu.onClick();
} else {
handleClose();
}
}}
{...menu}
/>
</div>
));
return (
<div data-testid="Menu">
<div
onClick={eventType === 'Click' ? handleOpen : undefined}
onKeyDown={eventType === 'Click' ? handleOpen : undefined}
onMouseEnter={eventType === 'MouseEnter' ? handleOpen : undefined}
onMouseLeave={eventType === 'MouseEnter' ? handleClose : undefined}
aria-hidden="true"
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
>
{children}
</div>
<Popper
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal={placement === 'top'}
placement={placement}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<div
onMouseEnter={eventType === 'MouseEnter' ? handleOpen : undefined}
onMouseLeave={eventType === 'MouseEnter' ? handleClose : undefined}
>
<MenuList autoFocusItem={open}>{menuList}</MenuList>
</div>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}
Example #11
Source File: global-search-field.tsx From mtcute with GNU Lesser General Public License v3.0 | 4 votes |
export function GlobalSearchField({ isMobile }: { isMobile: boolean }): React.ReactElement {
const classes = useStyles()
const allObjects: {
allTlObject: GraphqlAllResponse<ExtendedTlObject>
} = useStaticQuery(graphql`
query {
allTlObject {
edges {
node {
id
prefix
type
name
}
}
}
}
`)
const [includeMtproto, setIncludeMtproto] = useLocalState('mtproto', false)
const { hits, query, onSearch } = useFuse(
allObjects.allTlObject.edges,
{
keys: ['node.name'],
includeMatches: true,
threshold: 0.3,
},
{ limit: 25 },
includeMtproto ? undefined : (it) => it.node.prefix !== 'mtproto/'
)
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const [open, setOpen] = useState(false)
const notFound = () => (
<>
<ErrorOutlineIcon className={classes.popupEmptyIcon} />
Nothing found
{!includeMtproto && (
<Button
variant="text"
size="small"
style={{
margin: '4px auto',
}}
onClick={() => {
setIncludeMtproto(true)
}}
>
Retry including MTProto objects
</Button>
)}
</>
)
const emptyField = () => (
<>
<SearchIcon className={classes.popupEmptyIcon} />
Start typing...
</>
)
const renderSearchItem = (
node: ExtendedTlObject,
matches: ReadonlyArray<Fuse.FuseResultMatch>
) => (
<ListItem
button
divider
component={Link}
to={`/${node.prefix}${node.type}/${node.name}`}
className={classes.popupListItem}
onClick={() => setOpen(false)}
key={node.id}
>
<ListItemAvatar>
<Avatar
style={{
backgroundColor:
node.type === 'class'
? blue[600]
: node.type === 'method'
? red[600]
: yellow[700],
}}
>
{node.type === 'class' ? (
<ClassIcon />
) : node.type === 'method' ? (
<FunctionsIcon />
) : (
<UnionIcon />
)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<>
{node.prefix}
<FuseHighlight
matches={matches}
value={node.name}
className={classes.searchItemMatch}
/>
</>
}
secondary={node.type}
/>
</ListItem>
)
const popupContent = (
<Paper className={classes.popup}>
{query.length <= 1 || !hits.length ? (
<div className={classes.popupEmpty}>
{query.length <= 1 ? emptyField() : notFound()}
</div>
) : (
<List disablePadding dense className={classes.popupList}>
{hits.map(({ item: { node }, matches }) =>
renderSearchItem(node, matches!)
)}
<div style={{ textAlign: 'center' }}>
<Button
variant="text"
size="small"
style={{
margin: '4px auto',
}}
onClick={() => {
setIncludeMtproto(!includeMtproto)
}}
>
{includeMtproto ? 'Hide' : 'Include'} MTProto
objects
</Button>
</div>
</List>
)}
</Paper>
)
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<>
<ActionBarSearchField
inputRef={setAnchorEl}
autoComplete="off"
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
onChange={onSearch}
/>
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom"
transition
style={{
width: isMobile ? '100%' : anchorEl?.clientWidth,
zIndex: 9999,
}}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
{popupContent}
</Fade>
)}
</Popper>
</>
</ClickAwayListener>
)
}
Example #12
Source File: Planet.tsx From react-planet with MIT License | 4 votes |
export function Planet(props: Props) {
const {
centerContent,
children,
open,
onClick,
mass,
tension,
friction,
orbitRadius,
rotation,
orbitStyle,
hideOrbit,
onClose,
autoClose,
dragablePlanet,
dragRadiusPlanet,
dragableSatellites,
dragRadiusSatellites,
bounceRadius,
bounce,
bounceOnOpen,
bounceOnClose,
bounceDirection,
satelliteOrientation,
} = props;
const classes = useStyles(props);
const { ref, height = 0, width = 0 } = useResizeObserver();
const [_open, setOpen] = React.useState(!!open);
React.useEffect(() => {
setOpen(!!open);
}, [open]);
var satellites: ReactElement<any>[] = [];
var satelliteCount = React.Children.count(children);
React.Children.forEach(children, (c, i) => {
satellites[i] = (
<Satellite
key={i}
index={i}
open={_open}
satelliteCount={satelliteCount}
planetHeight={height}
planetWidth={width}
mass={mass ? mass : DEFAULT_MASS}
friction={friction ? friction : DEFAULT_FRICTION}
tension={tension ? tension : DEFAULT_TENSTION}
orbitRadius={orbitRadius ? orbitRadius : DEFAULT_RADIUS}
rotation={rotation ? rotation : DEFAULT_ROTATION}
dragable={!!dragableSatellites}
dragRadius={dragRadiusSatellites}
orientation={satelliteOrientation}
>
{c}
</Satellite>
);
});
const onPlanet = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (onClick) {
onClick(e);
} else {
if (_open && autoClose) {
setOpen(false);
if (onClose) {
onClose(e);
}
} else {
setOpen(true);
}
}
};
const onClickAway = (e: React.MouseEvent<Document, MouseEvent>) => {
if (autoClose) {
setOpen(false);
}
if (onClose && _open) {
onClose(e);
}
};
return (
<ClickAwayListener onClickAway={onClickAway}>
<div className={classes.root}>
{!hideOrbit && (
<Orbit
open={_open}
orbitStyle={orbitStyle}
planetHeight={height}
planetWidth={width}
mass={mass ? mass : DEFAULT_MASS}
friction={friction ? friction : DEFAULT_FRICTION}
tension={tension ? tension : DEFAULT_TENSTION}
orbitRadius={orbitRadius ? orbitRadius : DEFAULT_RADIUS}
/>
)}
<>{satellites}</>
<div className={classes.planetContent} onClick={onPlanet}>
<DragableContainer
on={
!!dragablePlanet || !!bounce || !!bounceOnOpen || !!bounceOnClose
}
dragable={!!dragablePlanet}
dragRadius={dragRadiusPlanet}
open={_open}
bounceRadius={bounceRadius}
bounceOnOpen={(bounce && !!!bounceOnClose) || bounceOnOpen}
bounceOnClose={(bounce && !!!bounceOnOpen) || bounceOnClose}
bounceDirection={bounceDirection}
>
<div ref={ref as any}>{centerContent}</div>
</DragableContainer>
</div>
</div>
</ClickAwayListener>
);
}
Example #13
Source File: index.tsx From uno-game with MIT License | 4 votes |
CardDeck: React.FC<CardDeckProps> = (props) => {
const { cards, player } = props
const { gameId } = useParams<{ gameId: string }>()
const {
isDraggingAnyCard,
} = useDragLayer((monitor) => ({
isDraggingAnyCard: monitor.isDragging(),
}))
const cardStore = useCardStore()
const socketStore = useSocketStore()
const socket = useSocket()
const classes = useStyles()
const customClasses = useCustomStyles({
limitedNameWidth: 70,
avatarTimerRemainingPercentage: buildPercentage(
socketStore.gameRoundRemainingTimeInSeconds as number,
socketStore.game?.maxRoundDurationInSeconds as number,
),
})
const getCardInclination = (index: number) => {
const isMiddleCard = Math.round(cards.length / 2) === index
const isBeforeMiddleCard = index < Math.round(cards.length / 2)
let inclination: number
if (isMiddleCard) {
inclination = 0
} else if (isBeforeMiddleCard) {
inclination = -Math.abs(index - Math.round(cards.length / 2))
} else {
inclination = Math.abs(Math.round(cards.length / 2) - index)
}
const delta = Device.isMobile ? 4 : 3
return inclination * delta
}
const getCardElevation = (index: number) => {
const isMiddleCard = Math.round(cards.length / 2) === index
let elevation: number
if (isMiddleCard) {
elevation = 0
} else {
elevation = -Math.abs(index - Math.round(cards.length / 2))
}
const delta = Device.isMobile ? 3 : 7
return elevation * delta
}
const onDragEnd = () => {
cardStore.setSelectedCards([])
}
const isCardSelected = (cardId: string) => !!cardStore?.selectedCards?.some(card => card.id === cardId)
const canBePartOfCurrentCombo = (cardType: CardTypes) => !!cardStore?.selectedCards?.some(card => card.type === cardType)
const toggleSelectedCard = (cardId: string) => {
const lastSelectedCards = cardStore.selectedCards
const selectedCard = cards.find(card => card.id === cardId)
const cardOnTopOfCardStack = (socketStore.game as Game).usedCards[0]
const selectedCardTypes = lastSelectedCards?.map(card => card.type)
const isAlreadySelected = isCardSelected(cardId)
if (isAlreadySelected) {
const cardsWithoutAlreadySelected = lastSelectedCards?.filter(card => card.id !== cardId)
if (cardOnTopOfCardStack.color === selectedCard?.color) {
if (cardsWithoutAlreadySelected[0] && cardsWithoutAlreadySelected[0].type === cardOnTopOfCardStack.type) {
cardStore.setSelectedCards(cardsWithoutAlreadySelected)
} else {
cardStore.setSelectedCards([])
}
} else {
cardStore.setSelectedCards(cardsWithoutAlreadySelected)
}
} else if ((selectedCard && selectedCardTypes?.includes(selectedCard.type)) || !selectedCardTypes?.length) {
cardStore.setSelectedCards([
...(lastSelectedCards || []),
selectedCard as CardData,
])
}
}
const unselectAllCards = () => {
cardStore.setSelectedCards([])
}
const handleClickOutsideCardDeck = () => {
if (cardStore.selectedCards.length > 0) {
unselectAllCards()
}
}
const toggleOnlineStatus = () => {
socket.toggleOnlineStatus(gameId)
}
return (
<ClickAwayListener
onClickAway={handleClickOutsideCardDeck}
>
<Grid
container
alignItems="flex-end"
justify="center"
className={classes.cardContainer}
>
<PlayerEffect
playerId={player?.id}
/>
<Zoom in={player?.status === "afk"}>
<Grid
container
className={classes.afkContainer}
>
<Grid
container
alignItems="center"
justify="center"
direction="column"
className={classes.afkContent}
>
<Typography
variant="body1"
className={classes.afkInfo}
>
We noticed you are afk, so we are making random plays
{" "}
automatically for you. In case you want to keep playing by
{" "}
yourself, click on the button below.
</Typography>
<Divider orientation="horizontal" size={2} />
<Button
variant="contained"
className={classes.afkButton}
onClick={toggleOnlineStatus}
>
I'M HERE
</Button>
</Grid>
</Grid>
</Zoom>
<Grid
container
className={classes.cardContent}
style={{
width: (cards?.length * CARD_WIDTH) + CARD_WIDTH,
}}
>
{cards?.map((card, index) => (
<DraggableCard
key={card.id}
card={card}
className={classes.card}
index={index}
style={{
transform: `rotate(${getCardInclination(index)}deg)`,
bottom: getCardElevation(index),
zIndex: (index + 2),
left: index * CARD_WIDTH,
}}
onClick={() => toggleSelectedCard(card.id)}
selected={isCardSelected(card.id)}
isDraggingAnyCard={isDraggingAnyCard}
isMoreThanOneCardBeingDragged={cardStore?.selectedCards?.length > 1}
onDragEnd={onDragEnd}
canBePartOfCurrentCombo={canBePartOfCurrentCombo(card.type)}
/>
))}
</Grid>
<Grid
container
justify="center"
alignItems="center"
className={classes.avatarContainer}
style={{
opacity: player?.isCurrentRoundPlayer ? 1 : 0.5,
}}
>
<Avatar
name={player?.name}
size="small"
className={player?.isCurrentRoundPlayer ? customClasses.avatarTimer : ""}
/>
<Divider orientation="vertical" size={2} />
<Grid item>
<Typography
variant="h3"
className={`${classes.title} ${customClasses.limitedName}`}
>
{player?.name}
</Typography>
{player?.id && (
<Typography
variant="h2"
className={classes.description}
>
(You)
</Typography>
)}
</Grid>
</Grid>
</Grid>
</ClickAwayListener>
)
}
Example #14
Source File: SavedSearchToolbar.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
SavedSearchToolbar: React.SFC<SavedSearchToolbarProps> = (props) => {
const { searchMode, refetchData, savedSearchCriteriaCallback, onSelect } = props;
const [selectedSavedSearch, setSelectedSavedSearch] = useState<number | null>(null);
const [optionsSelected, setOptionsSelected] = useState(false);
const [fixedSearches, setFixedSearches] = useState<any>([]);
const [searchesCount, setSearchesCount] = useState<any>({});
const [anchorEl, setAnchorEl] = useState(null);
const Ref = useRef(null);
const open = Boolean(anchorEl);
const variables = { organizationId: getUserSession('organizationId') };
const { data: collectionCount } = useSubscription(COLLECTION_COUNT_SUBSCRIPTION, { variables });
const { data: countData } = useQuery<any>(SEARCHES_COUNT, {
variables,
});
useEffect(() => {
if (countData) {
const collectionStats = JSON.parse(countData.collectionStats);
if (collectionStats[variables.organizationId]) {
setSearchesCount(collectionStats[variables.organizationId]);
}
}
}, [countData]);
useEffect(() => {
if (collectionCount) {
const countDataSubscription = JSON.parse(collectionCount.collectionCount);
setSearchesCount(countDataSubscription.collection);
}
}, [collectionCount]);
// default query variables
const queryVariables = {
filter: { isReserved: true },
opts: {},
};
// remove selected searches on search
if (searchMode && selectedSavedSearch) {
setSelectedSavedSearch(null);
}
const { loading, error, refetch } = useQuery<any>(SAVED_SEARCH_QUERY, {
variables: queryVariables,
onCompleted: (data) => {
setFixedSearches(data.savedSearches);
},
});
const handlerSavedSearchCriteria = (
savedSearchCriteria: string | null,
savedSearchId: number | null
) => {
savedSearchCriteriaCallback(savedSearchCriteria, savedSearchId);
setSelectedSavedSearch(savedSearchId);
};
const handleAdditionalSavedSearch = (search: any) => {
const replaceSearchIndex = fixedSearches
.map((savedSearch: any) => savedSearch.id)
.indexOf(search.id);
const fixedSearchesCopy = JSON.parse(JSON.stringify(fixedSearches));
if (replaceSearchIndex !== -1) {
[fixedSearchesCopy[replaceSearchIndex], fixedSearchesCopy[2]] = [
fixedSearches[2],
fixedSearches[replaceSearchIndex],
];
setFixedSearches(fixedSearchesCopy);
}
handlerSavedSearchCriteria(search.args, search.id);
};
useEffect(() => {
// display created searches
if (refetchData.savedSearches) {
refetch();
handleAdditionalSavedSearch(refetchData.savedSearches);
}
}, [refetchData.savedSearches]);
if (loading) return <Loading />;
if (error) {
setErrorMessage(error);
return <div>error</div>;
}
const savedSearchList = fixedSearches.slice(0, 3).map((savedSearch: any) => {
// set the selected class if the button is clicked
const labelClass = [styles.SavedSearchItemLabel];
const countClass = [styles.SavedSearchCount];
if (savedSearch.id === selectedSavedSearch) {
labelClass.push(styles.SavedSearchItemSelected);
countClass.push(styles.SavedSearchSelectedCount);
}
const count = searchesCount[savedSearch.shortcode] ? searchesCount[savedSearch.shortcode] : 0;
return (
<div
data-testid="savedSearchDiv"
className={styles.SavedSearchItem}
key={savedSearch.id}
onClick={() => {
handlerSavedSearchCriteria(savedSearch.args, savedSearch.id);
onSelect();
}}
onKeyDown={() => {
handlerSavedSearchCriteria(savedSearch.args, savedSearch.id);
onSelect();
}}
aria-hidden="true"
>
<div className={labelClass.join(' ')}>{savedSearch.shortcode}</div>
<Tooltip title={count} placement="right">
<div className={countClass.join(' ')}>{numberToAbbreviation(count)}</div>
</Tooltip>
</div>
);
});
const handleClickAway = () => {
setAnchorEl(null);
setOptionsSelected(false);
};
const additionalOptions = (
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom"
transition
className={styles.PopperContainer}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper elevation={3} className={styles.Popper}>
{fixedSearches.slice(3, 6).map((search: any) => {
const count = searchesCount[search.shortcode] ? searchesCount[search.shortcode] : 0;
return (
<div
key={search.id}
className={styles.LabelContainer}
onClick={() => handleAdditionalSavedSearch(search)}
aria-hidden="true"
>
<span className={styles.Label}>{search.shortcode}</span>
<span className={styles.Count}>{numberToAbbreviation(count)}</span>
</div>
);
})}
</Paper>
</Fade>
)}
</Popper>
);
return (
<div className={styles.SavedSearchToolbar}>
<div className={styles.SaveSearchContainer}>{savedSearchList}</div>
<div className={styles.MoreLink}>
<ClickAwayListener onClickAway={handleClickAway}>
<IconButton
onClick={() => {
setAnchorEl(Ref.current);
setOptionsSelected(true);
}}
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
size="small"
ref={Ref}
>
{optionsSelected ? (
<OptionsIconSelected className={styles.OptionsIcon} />
) : (
<OptionsIcon className={styles.OptionsIcon} />
)}
</IconButton>
</ClickAwayListener>
{additionalOptions}
</div>
</div>
);
}
Example #15
Source File: ContactBar.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ContactBar: React.SFC<ContactBarProps> = (props) => {
const {
contactId,
collectionId,
contactBspStatus,
lastMessageTime,
contactStatus,
displayName,
handleAction,
isSimulator,
} = props;
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const history = useHistory();
const [showCollectionDialog, setShowCollectionDialog] = useState(false);
const [showFlowDialog, setShowFlowDialog] = useState(false);
const [showBlockDialog, setShowBlockDialog] = useState(false);
const [showClearChatDialog, setClearChatDialog] = useState(false);
const [addContactsDialogShow, setAddContactsDialogShow] = useState(false);
const [showTerminateDialog, setShowTerminateDialog] = useState(false);
const { t } = useTranslation();
// get collection list
const [getCollections, { data: collectionsData }] = useLazyQuery(GET_COLLECTIONS, {
variables: setVariables(),
});
// get the published flow list
const [getFlows, { data: flowsData }] = useLazyQuery(GET_FLOWS, {
variables: setVariables({
status: FLOW_STATUS_PUBLISHED,
}),
fetchPolicy: 'network-only', // set for now, need to check cache issue
});
// get contact collections
const [getContactCollections, { data }] = useLazyQuery(GET_CONTACT_COLLECTIONS, {
variables: { id: contactId },
fetchPolicy: 'cache-and-network',
});
useEffect(() => {
if (contactId) {
getContactCollections();
}
}, [contactId]);
// mutation to update the contact collections
const [updateContactCollections] = useMutation(UPDATE_CONTACT_COLLECTIONS, {
onCompleted: (result: any) => {
const { numberDeleted, contactGroups } = result.updateContactGroups;
const numberAdded = contactGroups.length;
let notification = `Added to ${numberAdded} collection${numberAdded === 1 ? '' : 's'}`;
if (numberDeleted > 0 && numberAdded > 0) {
notification = `Added to ${numberDeleted} collection${
numberDeleted === 1 ? '' : 's and'
} removed from ${numberAdded} collection${numberAdded === 1 ? '' : 's '}`;
} else if (numberDeleted > 0) {
notification = `Removed from ${numberDeleted} collection${numberDeleted === 1 ? '' : 's'}`;
}
setNotification(notification);
},
refetchQueries: [{ query: GET_CONTACT_COLLECTIONS, variables: { id: contactId } }],
});
const [blockContact] = useMutation(UPDATE_CONTACT, {
onCompleted: () => {
setNotification(t('Contact blocked successfully.'));
},
refetchQueries: [{ query: SEARCH_QUERY, variables: SEARCH_QUERY_VARIABLES }],
});
const [addFlow] = useMutation(ADD_FLOW_TO_CONTACT, {
onCompleted: () => {
setNotification(t('Flow started successfully.'));
},
onError: (error) => {
setErrorMessage(error);
},
});
const [addFlowToCollection] = useMutation(ADD_FLOW_TO_COLLECTION, {
onCompleted: () => {
setNotification(t('Your flow will start in a couple of minutes.'));
},
});
// mutation to clear the chat messages of the contact
const [clearMessages] = useMutation(CLEAR_MESSAGES, {
variables: { contactId },
onCompleted: () => {
setClearChatDialog(false);
setNotification(t('Conversation cleared for this contact.'), 'warning');
},
});
let collectionOptions = [];
let flowOptions = [];
let initialSelectedCollectionIds: Array<any> = [];
let selectedCollectionsName;
let selectedCollections: any = [];
let assignedToCollection: any = [];
if (data) {
const { groups } = data.contact.contact;
initialSelectedCollectionIds = groups.map((group: any) => group.id);
selectedCollections = groups.map((group: any) => group.label);
selectedCollectionsName = shortenMultipleItems(selectedCollections);
assignedToCollection = groups.map((group: any) => group.users.map((user: any) => user.name));
assignedToCollection = Array.from(new Set([].concat(...assignedToCollection)));
assignedToCollection = shortenMultipleItems(assignedToCollection);
}
if (collectionsData) {
collectionOptions = collectionsData.groups;
}
if (flowsData) {
flowOptions = flowsData.flows;
}
let dialogBox = null;
const handleCollectionDialogOk = (selectedCollectionIds: any) => {
const finalSelectedCollections = selectedCollectionIds.filter(
(selectedCollectionId: any) => !initialSelectedCollectionIds.includes(selectedCollectionId)
);
const finalRemovedCollections = initialSelectedCollectionIds.filter(
(gId: any) => !selectedCollectionIds.includes(gId)
);
if (finalSelectedCollections.length > 0 || finalRemovedCollections.length > 0) {
updateContactCollections({
variables: {
input: {
contactId,
addGroupIds: finalSelectedCollections,
deleteGroupIds: finalRemovedCollections,
},
},
});
}
setShowCollectionDialog(false);
};
const handleCollectionDialogCancel = () => {
setShowCollectionDialog(false);
};
if (showCollectionDialog) {
dialogBox = (
<SearchDialogBox
selectedOptions={initialSelectedCollectionIds}
title={t('Add contact to collection')}
handleOk={handleCollectionDialogOk}
handleCancel={handleCollectionDialogCancel}
options={collectionOptions}
/>
);
}
const handleFlowSubmit = (flowId: any) => {
const flowVariables: any = {
flowId,
};
if (contactId) {
flowVariables.contactId = contactId;
addFlow({
variables: flowVariables,
});
}
if (collectionId) {
flowVariables.groupId = collectionId;
addFlowToCollection({
variables: flowVariables,
});
}
setShowFlowDialog(false);
};
const closeFlowDialogBox = () => {
setShowFlowDialog(false);
};
if (showFlowDialog) {
dialogBox = (
<DropdownDialog
title={t('Select flow')}
handleOk={handleFlowSubmit}
handleCancel={closeFlowDialogBox}
options={flowOptions}
placeholder={t('Select flow')}
description={t('The contact will be responded as per the messages planned in the flow.')}
/>
);
}
const handleClearChatSubmit = () => {
clearMessages();
setClearChatDialog(false);
handleAction();
};
if (showClearChatDialog) {
const bodyContext =
'All the conversation data for this contact will be deleted permanently from Glific. This action cannot be undone. However, you should be able to access it in reports if you have backup configuration enabled.';
dialogBox = (
<DialogBox
title="Are you sure you want to clear all conversation for this contact?"
handleOk={handleClearChatSubmit}
handleCancel={() => setClearChatDialog(false)}
alignButtons="center"
buttonOk="YES, CLEAR"
colorOk="secondary"
buttonCancel="MAYBE LATER"
>
<p className={styles.DialogText}>{bodyContext}</p>
</DialogBox>
);
}
const handleBlock = () => {
blockContact({
variables: {
id: contactId,
input: {
status: 'BLOCKED',
},
},
});
};
if (showBlockDialog) {
dialogBox = (
<DialogBox
title="Do you want to block this contact"
handleOk={handleBlock}
handleCancel={() => setShowBlockDialog(false)}
alignButtons="center"
colorOk="secondary"
>
<p className={styles.DialogText}>
You will not be able to view their chats and interact with them again
</p>
</DialogBox>
);
}
if (showTerminateDialog) {
dialogBox = <TerminateFlow contactId={contactId} setDialog={setShowTerminateDialog} />;
}
let flowButton: any;
const blockContactButton = contactId ? (
<Button
data-testid="blockButton"
className={styles.ListButtonDanger}
color="secondary"
disabled={isSimulator}
onClick={() => setShowBlockDialog(true)}
>
{isSimulator ? (
<BlockDisabledIcon className={styles.Icon} />
) : (
<BlockIcon className={styles.Icon} />
)}
Block Contact
</Button>
) : null;
if (collectionId) {
flowButton = (
<Button
data-testid="flowButton"
className={styles.ListButtonPrimary}
onClick={() => {
getFlows();
setShowFlowDialog(true);
}}
>
<FlowIcon className={styles.Icon} />
Start a flow
</Button>
);
} else if (
contactBspStatus &&
status.includes(contactBspStatus) &&
!is24HourWindowOver(lastMessageTime)
) {
flowButton = (
<Button
data-testid="flowButton"
className={styles.ListButtonPrimary}
onClick={() => {
getFlows();
setShowFlowDialog(true);
}}
>
<FlowIcon className={styles.Icon} />
Start a flow
</Button>
);
} else {
let toolTip = 'Option disabled because the 24hr window expired';
let disabled = true;
// if 24hr window expired & contact type HSM. we can start flow with template msg .
if (contactBspStatus === 'HSM') {
toolTip =
'Since the 24-hour window has passed, the contact will only receive a template message.';
disabled = false;
}
flowButton = (
<Tooltip title={toolTip} placement="right">
<span>
<Button
data-testid="disabledFlowButton"
className={styles.ListButtonPrimary}
disabled={disabled}
onClick={() => {
getFlows();
setShowFlowDialog(true);
}}
>
{disabled ? (
<FlowUnselectedIcon className={styles.Icon} />
) : (
<FlowIcon className={styles.Icon} />
)}
Start a flow
</Button>
</span>
</Tooltip>
);
}
const terminateFLows = contactId ? (
<Button
data-testid="terminateButton"
className={styles.ListButtonPrimary}
onClick={() => {
setShowTerminateDialog(!showTerminateDialog);
}}
>
<TerminateFlowIcon className={styles.Icon} />
Terminate flows
</Button>
) : null;
const viewDetails = contactId ? (
<Button
className={styles.ListButtonPrimary}
disabled={isSimulator}
data-testid="viewProfile"
onClick={() => {
history.push(`/contact-profile/${contactId}`);
}}
>
{isSimulator ? (
<ProfileDisabledIcon className={styles.Icon} />
) : (
<ProfileIcon className={styles.Icon} />
)}
View contact profile
</Button>
) : (
<Button
className={styles.ListButtonPrimary}
data-testid="viewContacts"
onClick={() => {
history.push(`/collection/${collectionId}/contacts`);
}}
>
<ProfileIcon className={styles.Icon} />
View details
</Button>
);
const addMember = contactId ? (
<>
<Button
data-testid="collectionButton"
className={styles.ListButtonPrimary}
onClick={() => {
getCollections();
setShowCollectionDialog(true);
}}
>
<AddContactIcon className={styles.Icon} />
Add to collection
</Button>
<Button
className={styles.ListButtonPrimary}
data-testid="clearChatButton"
onClick={() => setClearChatDialog(true)}
>
<ClearConversation className={styles.Icon} />
Clear conversation
</Button>
</>
) : (
<Button
data-testid="collectionButton"
className={styles.ListButtonPrimary}
onClick={() => {
setAddContactsDialogShow(true);
}}
>
<AddContactIcon className={styles.Icon} />
Add contact
</Button>
);
if (addContactsDialogShow) {
dialogBox = (
<AddContactsToCollection collectionId={collectionId} setDialog={setAddContactsDialogShow} />
);
}
const popper = (
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-start"
transition
className={styles.Popper}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper elevation={3} className={styles.Container}>
{viewDetails}
{flowButton}
{addMember}
{terminateFLows}
{blockContactButton}
</Paper>
</Fade>
)}
</Popper>
);
const handleConfigureIconClick = (event: any) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
let contactCollections: any;
if (selectedCollections.length > 0) {
contactCollections = (
<div className={styles.ContactCollections}>
<span className={styles.CollectionHeading}>Collections</span>
<span className={styles.CollectionsName} data-testid="collectionNames">
{selectedCollectionsName}
</span>
</div>
);
}
const getTitleAndIconForSmallScreen = (() => {
const { location } = history;
if (location.pathname.includes('collection')) {
return CollectionIcon;
}
if (location.pathname.includes('saved-searches')) {
return SavedSearchIcon;
}
return ChatIcon;
})();
// CONTACT: display session timer & Assigned to
const IconComponent = getTitleAndIconForSmallScreen;
const sessionAndCollectionAssignedTo = (
<>
{contactId ? (
<div className={styles.SessionTimerContainer}>
<div className={styles.SessionTimer} data-testid="sessionTimer">
<span>Session Timer</span>
<Timer
time={lastMessageTime}
contactStatus={contactStatus}
contactBspStatus={contactBspStatus}
/>
</div>
<div>
{assignedToCollection ? (
<>
<span className={styles.CollectionHeading}>Assigned to</span>
<span className={styles.CollectionsName}>{assignedToCollection}</span>
</>
) : null}
</div>
</div>
) : null}
<div className={styles.Chat} onClick={() => showChats()} aria-hidden="true">
<IconButton className={styles.MobileIcon}>
<IconComponent />
</IconButton>
</div>
</>
);
// COLLECTION: display contact info & Assigned to
let collectionStatus: any;
if (collectionId) {
collectionStatus = <CollectionInformation collectionId={collectionId} />;
}
return (
<Toolbar className={styles.ContactBar} color="primary">
<div className={styles.ContactBarWrapper}>
<div className={styles.ContactInfoContainer}>
<div className={styles.ContactInfoWrapper}>
<div className={styles.InfoWrapperRight}>
<div className={styles.ContactDetails}>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<div
className={styles.Configure}
data-testid="dropdownIcon"
onClick={handleConfigureIconClick}
onKeyPress={handleConfigureIconClick}
aria-hidden
>
<DropdownIcon />
</div>
</ClickAwayListener>
<Typography
className={styles.Title}
variant="h6"
noWrap
data-testid="beneficiaryName"
>
{displayName}
</Typography>
</div>
{contactCollections}
</div>
{collectionStatus}
{sessionAndCollectionAssignedTo}
</div>
</div>
</div>
{popper}
{dialogBox}
</Toolbar>
);
}
Example #16
Source File: ChatInput.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ChatInput: React.SFC<ChatInputProps> = (props) => {
const {
onSendMessage,
contactBspStatus,
contactStatus,
additionalStyle,
handleHeightChange,
isCollection,
lastMessageTime,
} = props;
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const [selectedTab, setSelectedTab] = useState('');
const [open, setOpen] = React.useState(false);
const [searchVal, setSearchVal] = useState('');
const [attachment, setAttachment] = useState(false);
const [attachmentAdded, setAttachmentAdded] = useState(false);
const [interactiveMessageContent, setInteractiveMessageContent] = useState<any>({});
const [attachmentType, setAttachmentType] = useState<any>();
const [attachmentURL, setAttachmentURL] = useState('');
const [variable, setVariable] = useState(false);
const [updatedEditorState, setUpdatedEditorState] = useState<any>();
const [selectedTemplate, setSelectedTemplate] = useState<any>();
const [variableParam, setVariableParam] = useState<any>([]);
const [recordedAudio, setRecordedAudio] = useState<any>('');
const [clearAudio, setClearAudio] = useState<any>(false);
const [uploading, setUploading] = useState(false);
const { t } = useTranslation();
const speedSends = 'Speed sends';
const templates = 'Templates';
const interactiveMsg = 'Interactive msg';
let uploadPermission = false;
let dialog;
const resetVariable = () => {
setUpdatedEditorState(undefined);
setEditorState(EditorState.createEmpty());
setSelectedTemplate(undefined);
setInteractiveMessageContent({});
setVariableParam([]);
};
const { data: permission } = useQuery(GET_ATTACHMENT_PERMISSION);
const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE, {
onCompleted: (data: any) => {
if (data) {
onSendMessage(
'',
data.createMessageMedia.messageMedia.id,
attachmentType,
selectedTemplate,
variableParam
);
setAttachmentAdded(false);
setAttachmentURL('');
setAttachmentType('');
resetVariable();
}
},
});
const [uploadMediaBlob] = useMutation(UPLOAD_MEDIA_BLOB, {
onCompleted: (data: any) => {
if (data) {
setAttachmentType('AUDIO');
createMediaMessage({
variables: {
input: {
caption: '',
sourceUrl: data.uploadBlob,
url: data.uploadBlob,
},
},
});
setClearAudio(true);
setRecordedAudio('');
setUploading(false);
}
},
onError: () => {
setNotification(t('Sorry, unable to upload audio.'), 'warning');
setUploading(false);
},
});
const submitMessage = async (message: string) => {
// let's check if we are sending voice recording
if (recordedAudio) {
// converting blob into base64 format as needed by backend
const reader = new FileReader();
reader.readAsDataURL(recordedAudio);
reader.onloadend = () => {
const base64String: any = reader.result;
// get the part without the tags
const media = base64String.split(',')[1];
// save media that will return an URL
uploadMediaBlob({
variables: {
media,
extension: 'mp3',
},
});
};
setUploading(true);
}
// check for an empty message or message with just spaces
if ((!message || /^\s*$/.test(message)) && !attachmentAdded) return;
// check if there is any attachment
if (attachmentAdded) {
createMediaMessage({
variables: {
input: {
caption: message,
sourceUrl: attachmentURL,
url: attachmentURL,
},
},
});
// check if type is list or quick replies
} else if (interactiveMessageContent && interactiveMessageContent.type) {
onSendMessage(
null,
null,
interactiveMessageContent.type.toUpperCase(),
null,
null,
Number(selectedTemplate.id)
);
// else the type will by default be text
} else {
onSendMessage(message, null, 'TEXT', selectedTemplate, variableParam);
}
resetVariable();
// Resetting the EditorState
setEditorState(
EditorState.moveFocusToEnd(
EditorState.push(editorState, ContentState.createFromText(''), 'remove-range')
)
);
};
const handleClick = (title: string) => {
// clear the search when tab is opened again
setSearchVal('');
if (selectedTab === title) {
setSelectedTab('');
} else {
setSelectedTab(title);
}
setOpen(selectedTab !== title);
};
const handleClickAway = () => {
setOpen(false);
setSelectedTab('');
};
const resetAttachment = () => {
setAttachmentAdded(false);
setAttachmentURL('');
setAttachmentType('');
};
const handleSelectText = (obj: any, isInteractiveMsg: boolean = false) => {
resetVariable();
// set selected template
let messageBody = obj.body;
if (isInteractiveMsg) {
const interactiveContent = JSON.parse(obj.interactiveContent);
messageBody = getInteractiveMessageBody(interactiveContent);
setInteractiveMessageContent(interactiveContent);
}
setSelectedTemplate(obj);
// Conversion from HTML text to EditorState
setEditorState(getEditorFromContent(messageBody));
// Add attachment if present
if (Object.prototype.hasOwnProperty.call(obj, 'MessageMedia') && obj.MessageMedia) {
const type = obj.type ? obj.type : obj.MessageMedia.type;
setAttachmentAdded(true);
setAttachmentURL(obj.MessageMedia.sourceUrl);
setAttachmentType(type);
} else {
resetAttachment();
}
// check if variable present
const isVariable = obj.body?.match(pattern);
if (isVariable) {
setVariable(true);
}
handleClickAway();
};
const handleCancel = () => {
resetAttachment();
resetVariable();
};
const updateEditorState = (body: string) => {
setUpdatedEditorState(body);
};
const variableParams = (params: Array<any>) => {
setVariableParam(params);
};
if (variable) {
const dialogProps = {
template: selectedTemplate,
setVariable,
handleCancel,
updateEditorState,
variableParams,
variableParam,
};
dialog = <AddVariables {...dialogProps} />;
}
const handleAudioRecording = (blob: any) => {
setRecordedAudio(blob);
setClearAudio(false);
};
const handleSearch = (e: any) => {
setSearchVal(e.target.value);
};
const quickSendButtons = (quickSendTypes: any) => {
const buttons = quickSendTypes.map((type: string) => (
<div
key={type}
data-testid="shortcutButton"
onClick={() => handleClick(type)}
aria-hidden="true"
className={clsx(styles.QuickSend, {
[styles.QuickSendSelected]: selectedTab === type,
})}
>
{type}
</div>
));
return <div className={styles.QuickSendButtons}>{buttons}</div>;
};
// determine what kind of messages we should display
let quickSendTypes: any = [];
if (contactBspStatus) {
switch (contactBspStatus) {
case 'SESSION':
quickSendTypes = [speedSends, interactiveMsg];
break;
case 'SESSION_AND_HSM':
quickSendTypes = [speedSends, templates, interactiveMsg];
break;
case 'HSM':
quickSendTypes = [templates];
break;
default:
break;
}
if (is24HourWindowOver(lastMessageTime)) {
quickSendTypes = [templates];
}
}
if ((contactStatus && contactStatus === 'INVALID') || contactBspStatus === 'NONE') {
return (
<div className={styles.ContactOptOutMessage}>
{t(
'Sorry, chat is unavailable with this contact at this moment because they aren’t opted in to your number.'
)}
</div>
);
}
if (isCollection) {
quickSendTypes = [speedSends, templates];
}
let audioOption: any;
// enable audio only if GCS is configured
if (permission && permission.attachmentsEnabled) {
if (!selectedTemplate && Object.keys(interactiveMessageContent).length === 0) {
audioOption = (
<VoiceRecorder
handleAudioRecording={handleAudioRecording}
clearAudio={clearAudio}
uploading={uploading}
isMicActive={recordedAudio !== ''}
/>
);
}
uploadPermission = true;
}
if (attachment) {
const dialogProps = {
attachmentType,
attachmentURL,
setAttachment,
setAttachmentAdded,
setAttachmentType,
setAttachmentURL,
uploadPermission,
};
dialog = <AddAttachment {...dialogProps} />;
}
return (
<Container
className={`${styles.ChatInput} ${additionalStyle}`}
data-testid="message-input-container"
>
{dialog}
<ClickAwayListener onClickAway={handleClickAway}>
<div className={styles.SendsContainer}>
{open ? (
<Fade in={open} timeout={200}>
<div className={styles.Popup}>
<ChatTemplates
isTemplate={selectedTab === templates}
isInteractiveMsg={selectedTab === interactiveMsg}
searchVal={searchVal}
handleSelectText={handleSelectText}
/>
<SearchBar
className={styles.ChatSearchBar}
handleChange={handleSearch}
onReset={() => setSearchVal('')}
searchMode
/>
</div>
</Fade>
) : null}
{quickSendButtons(quickSendTypes)}
</div>
</ClickAwayListener>
<div
className={clsx(styles.ChatInputElements, {
[styles.Unrounded]: selectedTab !== '',
[styles.Rounded]: selectedTab === '',
})}
>
<WhatsAppEditor
editorState={updatedEditorState ? getEditorFromContent(updatedEditorState) : editorState}
setEditorState={setEditorState}
sendMessage={submitMessage}
handleHeightChange={handleHeightChange}
readOnly={
(selectedTemplate !== undefined && selectedTemplate.isHsm) ||
Object.keys(interactiveMessageContent).length !== 0
}
/>
{selectedTemplate || Object.keys(interactiveMessageContent).length > 0 ? (
<Tooltip title={t('Remove message')} placement="top">
<IconButton
className={updatedEditorState ? styles.CrossIcon : styles.CrossIconWithVariable}
onClick={() => {
resetVariable();
resetAttachment();
}}
>
<CrossIcon />
</IconButton>
</Tooltip>
) : null}
{updatedEditorState ? (
<IconButton
className={styles.VariableIcon}
onClick={() => {
setVariable(!variable);
}}
>
<VariableIcon />
</IconButton>
) : null}
{audioOption}
<IconButton
className={styles.AttachmentIcon}
onClick={() => {
setAttachment(!attachment);
}}
>
{attachment || attachmentAdded ? <AttachmentIconSelected /> : <AttachmentIcon />}
</IconButton>
<div className={styles.SendButtonContainer}>
<Button
className={styles.SendButton}
data-testid="sendButton"
variant="contained"
color="primary"
disableElevation
onClick={() => {
if (updatedEditorState) {
submitMessage(updatedEditorState);
} else {
submitMessage(getPlainTextFromEditor(editorState));
}
}}
disabled={
(!editorState.getCurrentContent().hasText() && !attachmentAdded && !recordedAudio) ||
uploading
}
>
<SendMessageIcon className={styles.SendIcon} />
</Button>
</div>
</div>
</Container>
);
}
Example #17
Source File: Simulator.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
Simulator: React.FC<SimulatorProps> = ({
showSimulator,
setSimulatorId,
simulatorIcon = true,
message,
flowSimulator,
isPreviewMessage,
resetMessage,
getFlowKeyword,
interactiveMessage,
showHeader = true,
hasResetButton = false,
}: SimulatorProps) => {
const [inputMessage, setInputMessage] = useState('');
const [simulatedMessages, setSimulatedMessage] = useState<any>();
const [isOpen, setIsOpen] = useState(false);
// Template listing
const [isDrawerOpen, setIsDrawerOpen] = useState<Boolean>(false);
const [selectedListTemplate, setSelectedListTemplate] = useState<any>(null);
const variables = { organizationId: getUserSession('organizationId') };
let messages: any[] = [];
let simulatorId = '';
const sender = {
name: '',
phone: '',
};
// chat messages will be shown on simulator
const isSimulatedMessage = true;
const sendMessage = (senderDetails: any, interactivePayload?: any, templateValue?: any) => {
const sendMessageText = inputMessage === '' && message ? message : inputMessage;
// check if send message text is not empty
if (!sendMessageText && !interactivePayload && !templateValue) return;
let type = 'text';
let payload: any = {};
let context: any = {};
if (interactivePayload) {
type = interactivePayload.payload.type;
payload = interactivePayload.payload;
delete payload.type;
context = interactivePayload.context;
} else if (templateValue) {
payload.text = templateValue;
} else {
payload.text = sendMessageText;
}
axios
.post(GUPSHUP_CALLBACK_URL, {
type: 'message',
payload: {
id: uuidv4(),
type,
payload,
sender: senderDetails,
context,
},
})
.catch((error) => {
// add log's
setLogs(
`sendMessageText:${sendMessageText} GUPSHUP_CALLBACK_URL:${GUPSHUP_CALLBACK_URL}`,
'info'
);
setLogs(error, 'error');
});
setInputMessage('');
// reset the message from floweditor for the next time
if (resetMessage) {
resetMessage();
}
};
const [loadSimulator, { data: allConversations, subscribeToMore }] = useLazyQuery(
SIMULATOR_SEARCH_QUERY,
{
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-only',
onCompleted: ({ search }) => {
if (subscribeToMore) {
const subscriptionVariables = { organizationId: getUserSession('organizationId') };
// message received subscription
subscribeToMore({
document: SIMULATOR_MESSAGE_RECEIVED_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateSimulatorConversations(prev, subscriptionData, 'RECEIVED'),
});
// message sent subscription
subscribeToMore({
document: SIMULATOR_MESSAGE_SENT_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateSimulatorConversations(prev, subscriptionData, 'SENT'),
});
if (search.length > 0) {
sendMessage({ name: search[0].contact.name, phone: search[0].contact.phone });
}
}
},
}
);
const { data: simulatorSubscribe }: any = useSubscription(SIMULATOR_RELEASE_SUBSCRIPTION, {
variables,
});
useEffect(() => {
if (simulatorSubscribe) {
try {
const userId = JSON.parse(simulatorSubscribe.simulatorRelease).simulator_release.user_id;
if (userId.toString() === getUserSession('id')) {
setSimulatorId(0);
}
} catch (error) {
setLogs('simulator release error', 'error');
}
}
}, [simulatorSubscribe]);
const [getSimulator, { data }] = useLazyQuery(GET_SIMULATOR, {
fetchPolicy: 'network-only',
onCompleted: (simulatorData) => {
if (simulatorData.simulatorGet) {
loadSimulator({ variables: getSimulatorVariables(simulatorData.simulatorGet.id) });
setSimulatorId(simulatorData.simulatorGet.id);
} else {
setNotification(
'Sorry! Simulators are in use by other staff members right now. Please wait for it to be idle',
'warning'
);
}
},
});
const [releaseSimulator]: any = useLazyQuery(RELEASE_SIMULATOR, {
fetchPolicy: 'network-only',
});
if (allConversations && data && data.simulatorGet) {
// currently setting the simulated contact as the default receiver
const simulatedContact = allConversations.search.filter(
(item: any) => item.contact.id === data.simulatorGet.id
);
if (simulatedContact.length > 0) {
messages = simulatedContact[0].messages;
simulatorId = simulatedContact[0].contact.id;
sender.name = simulatedContact[0].contact.name;
sender.phone = simulatedContact[0].contact.phone;
}
}
const getStyleForDirection = (direction: string, isInteractive: boolean): string => {
const simulatorClasses = [styles.ReceivedMessage, styles.InteractiveReceivedMessage];
if (isInteractive && direction === 'received') {
return simulatorClasses.join(' ');
}
if (direction === 'send') {
return styles.SendMessage;
}
return styles.ReceivedMessage;
};
const releaseUserSimulator = () => {
releaseSimulator();
setSimulatorId(0);
};
const handleOpenListReplyDrawer = (items: any) => {
setSelectedListTemplate(items);
setIsDrawerOpen(true);
};
const sendMediaMessage = (type: string, payload: any) => {
axios
.post(GUPSHUP_CALLBACK_URL, {
type: 'message',
payload: {
id: uuidv4(),
type,
payload,
sender: {
// this number will be the simulated contact number
phone: data ? data.simulatorGet?.phone : '',
name: data ? data.simulatorGet?.name : '',
},
},
})
.catch((error) => {
// add log's
setLogs(`sendMediaMessage:${type} GUPSHUP_CALLBACK_URL:${GUPSHUP_CALLBACK_URL}`, 'info');
setLogs(error, 'error');
});
};
const renderMessage = (
messageObject: any,
direction: string,
index: number,
isInteractive: boolean = false
) => {
const { insertedAt, type, media, location, interactiveContent, bspMessageId, templateType } =
messageObject;
const messageType = isInteractive ? templateType : type;
const { body, buttons } = WhatsAppTemplateButton(isInteractive ? '' : messageObject.body);
// Checking if interactive content is present then only parse to JSON
const content = interactiveContent && JSON.parse(interactiveContent);
let isInteractiveContentPresent = false;
let template;
if (content) {
isInteractiveContentPresent = !!Object.entries(content).length;
if (isInteractiveContentPresent && messageType === INTERACTIVE_LIST) {
template = (
<>
<ListReplyTemplate
{...content}
bspMessageId={bspMessageId}
showHeader={showHeader}
component={SimulatorTemplate}
onGlobalButtonClick={handleOpenListReplyDrawer}
/>
<TimeComponent direction={direction} insertedAt={insertedAt} />
</>
);
}
if (isInteractiveContentPresent && messageType === INTERACTIVE_QUICK_REPLY) {
template = (
<QuickReplyTemplate
{...content}
isSimulator
showHeader={showHeader}
disabled={isInteractive}
bspMessageId={bspMessageId}
onQuickReplyClick={(value: any) => sendMessage(sender, value)}
/>
);
}
}
return (
<div key={index}>
<div className={getStyleForDirection(direction, isInteractiveContentPresent)}>
{isInteractiveContentPresent && direction !== 'send' ? (
template
) : (
<>
<ChatMessageType
type={messageType}
media={media}
body={body}
location={location}
isSimulatedMessage={isSimulatedMessage}
/>
<TimeComponent direction={direction} insertedAt={insertedAt} />
</>
)}
</div>
<div className={styles.TemplateButtons}>
<TemplateButtons
template={buttons}
callbackTemplateButtonClick={(value: string) => sendMessage(sender, null, value)}
isSimulator
/>
</div>
</div>
);
};
const getChatMessage = () => {
const chatMessage = messages
.map((simulatorMessage: any, index: number) => {
if (simulatorMessage.receiver.id === simulatorId) {
return renderMessage(simulatorMessage, 'received', index);
}
return renderMessage(simulatorMessage, 'send', index);
})
.reverse();
setSimulatedMessage(chatMessage);
};
const getPreviewMessage = () => {
if (message && message.type) {
const previewMessage = renderMessage(message, 'received', 0);
if (['STICKER', 'AUDIO'].includes(message.type)) {
setSimulatedMessage(previewMessage);
} else if (message.body || message.media?.caption) {
setSimulatedMessage(previewMessage);
} else {
// To get rid of empty body and media caption for preview HSM
setSimulatedMessage('');
}
}
if (interactiveMessage) {
const { templateType, interactiveContent } = interactiveMessage;
const previewMessage = renderMessage(interactiveMessage, 'received', 0, true);
setSimulatedMessage(previewMessage);
if (templateType === INTERACTIVE_LIST) {
const { items } = JSON.parse(interactiveContent);
setSelectedListTemplate(items);
} else {
setIsDrawerOpen(false);
}
}
};
// to display only preview for template
useEffect(() => {
if (isPreviewMessage) {
getPreviewMessage();
}
}, [message]);
// for loading conversation
useEffect(() => {
if (allConversations && data) {
getChatMessage();
}
}, [data, allConversations]);
// for sending message to Gupshup
useEffect(() => {
if (!isPreviewMessage && message && data) {
sendMessage(sender);
}
}, [message]);
useEffect(() => {
if (isPreviewMessage && interactiveMessage) {
getPreviewMessage();
}
// Cleaning up simulator when switching between templates
if (!interactiveMessage) {
setSimulatedMessage(null);
setIsDrawerOpen(false);
}
}, [interactiveMessage]);
const messageRef = useCallback(
(node: any) => {
if (node) {
const nodeCopy = node;
setTimeout(() => {
nodeCopy.scrollTop = node.scrollHeight;
}, 100);
}
},
[messages]
);
const handleAttachmentClick = (media: any) => {
const { name: type, payload } = media;
const mediaUrl = document.querySelector('#media');
if (mediaUrl) {
const url = mediaUrl.getAttribute('data-url');
if (url) {
payload.url = url;
}
}
sendMediaMessage(type, payload);
setIsOpen(false);
};
const handleListReplyDrawerClose = () => {
setIsDrawerOpen(false);
setSelectedListTemplate(null);
};
const handleListDrawerItemClick = (payloadObject: any) => {
sendMessage(sender, payloadObject);
handleListReplyDrawerClose();
};
const dropdown = (
<ClickAwayListener onClickAway={() => setIsOpen(false)}>
<div className={styles.Dropdown} id="media">
{SAMPLE_MEDIA_FOR_SIMULATOR.map((media: any) => (
<Button
onClick={() => handleAttachmentClick(media)}
key={media.id}
className={styles.AttachmentOptions}
>
<MessageType type={media.id} color="dark" />
</Button>
))}
</div>
</ClickAwayListener>
);
const simulator = (
<Draggable>
<div className={styles.SimContainer}>
<div>
<div id="simulator" className={styles.Simulator}>
{!isPreviewMessage && (
<>
<ClearIcon
className={styles.ClearIcon}
onClick={() => {
releaseUserSimulator();
}}
data-testid="clearIcon"
/>
{hasResetButton && (
<ResetIcon
data-testid="resetIcon"
className={styles.ResetIcon}
onClick={() => {
if (getFlowKeyword) {
getFlowKeyword();
}
}}
/>
)}
</>
)}
<div className={styles.Screen}>
<div className={styles.Header}>
<ArrowBackIcon />
<img src={DefaultWhatsappImage} alt="default" />
<span data-testid="beneficiaryName">Beneficiary</span>
<div>
<VideocamIcon />
<CallIcon />
<MoreVertIcon />
</div>
</div>
<div className={styles.Messages} ref={messageRef} data-testid="simulatedMessages">
{simulatedMessages}
</div>
{isDrawerOpen && <div className={styles.BackgroundTint} />}
<div className={styles.Controls}>
<div>
<InsertEmoticonIcon className={styles.Icon} />
<input
type="text"
data-testid="simulatorInput"
onKeyPress={(event: any) => {
if (event.key === 'Enter') {
sendMessage(sender);
}
}}
value={inputMessage}
placeholder="Type a message"
disabled={isPreviewMessage}
onChange={(event) => setInputMessage(event.target.value)}
/>
<AttachFileIcon
data-testid="attachment"
className={styles.AttachFileIcon}
onClick={() => setIsOpen(!isOpen)}
/>
{isOpen ? dropdown : null}
<CameraAltIcon className={styles.Icon} />
</div>
<Button
variant="contained"
color="primary"
className={styles.SendButton}
disabled={isPreviewMessage}
onClick={() => sendMessage(sender)}
>
<MicIcon />
</Button>
</div>
{isDrawerOpen && (
<ListReplyTemplateDrawer
drawerTitle="Items"
items={selectedListTemplate}
disableSend={!!interactiveMessage}
onItemClick={handleListDrawerItemClick}
onDrawerClose={handleListReplyDrawerClose}
/>
)}
</div>
</div>
</div>
</div>
</Draggable>
);
const handleSimulator = () => {
// check for the flowkeyword from floweditor
if (getFlowKeyword) {
getFlowKeyword();
}
getSimulator();
};
return (
<>
{showSimulator && simulator}
{simulatorIcon && (
<SimulatorIcon
data-testid="simulatorIcon"
className={showSimulator ? styles.SimulatorIconClicked : styles.SimulatorIconNormal}
onClick={() => {
if (showSimulator) {
releaseUserSimulator();
} else {
handleSimulator();
}
}}
/>
)}
{flowSimulator && (
<div className={styles.PreviewButton}>
<FormButton
variant="outlined"
color="primary"
data-testid="previewButton"
className={styles.Button}
onClick={() => {
if (showSimulator) {
releaseUserSimulator();
} else {
handleSimulator();
}
}}
>
Preview
{showSimulator && <CancelOutlinedIcon className={styles.CrossIcon} />}
</FormButton>
</div>
)}
</>
);
}
Example #18
Source File: WhatsAppEditor.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
WhatsAppEditor: React.SFC<WhatsAppEditorProps> = (props) => {
const { setEditorState, sendMessage, editorState, handleHeightChange, readOnly = false } = props;
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const { t } = useTranslation();
const handleChange = (editorStateChange: any) => {
setEditorState(editorStateChange);
};
const updateValue = (input: any, isEmoji: boolean = false) => {
const editorContentState = editorState.getCurrentContent();
const editorSelectionState: any = editorState.getSelection();
const ModifiedContent = Modifier.replaceText(
editorContentState,
editorSelectionState,
isEmoji ? input.native : input
);
let updatedEditorState = EditorState.push(editorState, ModifiedContent, 'insert-characters');
if (!isEmoji) {
const editorSelectionStateMod = updatedEditorState.getSelection();
const updatedSelection = editorSelectionStateMod.merge({
anchorOffset: editorSelectionStateMod.getAnchorOffset() - 1,
focusOffset: editorSelectionStateMod.getFocusOffset() - 1,
});
updatedEditorState = EditorState.forceSelection(updatedEditorState, updatedSelection);
}
setEditorState(updatedEditorState);
};
const handleKeyCommand = (command: string, editorStateChange: any) => {
// On enter, submit. Otherwise, deal with commands like normal.
if (command === 'enter') {
// Convert Draft.js to WhatsApp
sendMessage(getPlainTextFromEditor(editorStateChange));
return 'handled';
}
if (command === 'underline') {
return 'handled';
}
if (command === 'bold') {
updateValue('**');
} else if (command === 'italic') {
updateValue('__');
} else {
const newState = RichUtils.handleKeyCommand(editorStateChange, command);
if (newState) {
setEditorState(newState);
return 'handled';
}
}
return 'not-handled';
};
const keyBindingFn = (e: any) => {
// Shift-enter is by default supported. Only 'enter' needs to be changed.
if (e.keyCode === 13 && !e.nativeEvent.shiftKey) {
return 'enter';
}
return getDefaultKeyBinding(e);
};
const handleClickAway = () => {
setShowEmojiPicker(false);
};
const emojiStyles: any = {
position: 'absolute',
bottom: '60px',
right: '-150px',
zIndex: 100,
};
if (window.innerWidth <= 768) {
emojiStyles.right = '5%';
}
return (
<>
<ReactResizeDetector
data-testid="resizer"
handleHeight
onResize={(width: any, height: any) => handleHeightChange(height - 40)} // 40 is the initial height
>
<div className={styles.Editor}>
<Editor
data-testid="editor"
editorState={editorState}
onChange={handleChange}
handleKeyCommand={handleKeyCommand}
keyBindingFn={keyBindingFn}
placeholder={t('Type a message...')}
readOnly={readOnly}
/>
</div>
</ReactResizeDetector>
<ClickAwayListener onClickAway={handleClickAway}>
<div>
<div className={styles.EmojiButton}>
<IconButton
data-testid="emoji-picker"
color="primary"
aria-label="pick emoji"
component="span"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
>
<span role="img" aria-label="pick emoji">
?
</span>
</IconButton>
</div>
{showEmojiPicker ? (
<Picker
data-testid="emoji-popup"
title={t('Pick your emoji…')}
emoji="point_up"
style={emojiStyles}
onSelect={(emoji) => updateValue(emoji, true)}
/>
) : null}
</div>
</ClickAwayListener>
</>
);
}
Example #19
Source File: EmojiInput.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
EmojiInput: React.FC<EmojiInputProps> = ({
field: { value, name, onBlur },
handleChange,
getEditorValue,
handleBlur,
...props
}: EmojiInputProps) => {
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const { t } = useTranslation();
const updateValue = (input: any, isEmoji = false) => {
const editorContentState = value.getCurrentContent();
const editorSelectionState: any = value.getSelection();
const ModifiedContent = Modifier.replaceText(
editorContentState,
editorSelectionState,
isEmoji ? input.native : input
);
let updatedEditorState = EditorState.push(value, ModifiedContent, 'insert-characters');
if (!isEmoji) {
const editorSelectionStateMod = updatedEditorState.getSelection();
const updatedSelection = editorSelectionStateMod.merge({
anchorOffset: editorSelectionStateMod.getAnchorOffset() - 1,
focusOffset: editorSelectionStateMod.getFocusOffset() - 1,
});
updatedEditorState = EditorState.forceSelection(updatedEditorState, updatedSelection);
}
props.form.setFieldValue(name, updatedEditorState);
};
const handleKeyCommand = (command: any, editorState: any) => {
if (command === 'underline') {
return 'handled';
}
if (command === 'bold') {
updateValue('**');
} else if (command === 'italic') {
updateValue('__');
} else {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
props.form.setFieldValue(name, newState);
return 'handled';
}
}
return 'not-handled';
};
const draftJsChange = (editorState: any) => {
if (handleChange) {
handleChange(getPlainTextFromEditor(props.form.values.example));
}
if (getEditorValue) {
getEditorValue(editorState);
} else {
props.form.setFieldValue(name, editorState);
}
};
const mentions = props.inputProp?.suggestions || [];
const [open, setOpen] = useState(false);
const [suggestions, setSuggestions] = useState(mentions);
const onOpenChange = (_open: boolean) => {
setOpen(_open);
};
const getSuggestions = useCallback(customSuggestionsFilter, []);
const onSearchChange = ({ value: searchValue }: { value: string }) => {
setSuggestions(getSuggestions(searchValue, mentions));
};
const inputProps = {
component: Editor,
editorState: value,
open,
readOnly: props.disabled,
suggestions,
onOpenChange,
onSearchChange,
handlePastedText: (text: string, html: string, editorState: EditorState) => {
const pastedBlocks = ContentState.createFromText(text).getBlockMap();
const newState = Modifier.replaceWithFragment(
editorState.getCurrentContent(),
editorState.getSelection(),
pastedBlocks
);
const newEditorState = EditorState.push(editorState, newState, 'insert-fragment');
draftJsChange(newEditorState);
return 'handled';
},
handleKeyCommand,
onBlur: handleBlur,
onChange: draftJsChange,
};
const editor = { inputComponent: DraftField, inputProps };
const emojiPicker = showEmojiPicker ? (
<Picker
data-testid="emoji-container"
title={t('Pick your emoji…')}
emoji="point_up"
style={{ position: 'absolute', top: '10px', right: '0px', zIndex: 2 }}
onSelect={(emojiValue) => updateValue(emojiValue, true)}
/>
) : (
''
);
const handleClickAway = () => {
setShowEmojiPicker(false);
};
const picker = (
<ClickAwayListener onClickAway={handleClickAway}>
<InputAdornment position="end" className={Styles.EmojiPosition}>
<IconButton
color="primary"
data-testid="emoji-picker"
aria-label="pick emoji"
component="span"
className={Styles.Emoji}
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
>
<span role="img" aria-label="pick emoji">
?
</span>
</IconButton>
{emojiPicker}
</InputAdornment>
</ClickAwayListener>
);
const input = (
<Input field={{ name, value, onBlur }} {...props} editor={editor} emojiPicker={picker} />
);
return input;
}
Example #20
Source File: Dropdown.tsx From halstack-react with Apache License 2.0 | 4 votes |
DxcDropdown = ({
options,
optionsIconPosition = "before",
icon,
iconPosition = "before",
label = "",
caretHidden = false,
onSelectOption,
expandOnHover = false,
margin,
size = "fitContent",
tabIndex = 0,
disabled = false,
}: DropdownPropsType): JSX.Element => {
const [width, setWidth] = useState();
const colorsTheme = useTheme();
const ref = useRef(null);
const handleResize = () => {
if (ref.current) setWidth(ref.current.offsetWidth);
};
useEffect(() => {
if (ref.current) {
ref.current.addEventListener("resize", handleResize);
handleResize();
}
return () => {
if (ref.current) ref.current.removeEventListener("resize", handleResize);
};
}, []);
const [anchorEl, setAnchorEl] = useState(null);
function handleClickListItem(event) {
setAnchorEl(event.currentTarget);
}
function handleMenuItemClick(option) {
setAnchorEl(null);
onSelectOption(option.value);
}
function handleClose() {
setAnchorEl(null);
}
const handleCloseOver = expandOnHover ? handleClose : undefined;
const UpArrowIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 14l5-5 5 5z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
const DownArrowIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 10l5 5 5-5z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
const labelComponent = (
<DropdownTriggerLabel iconPosition={iconPosition} label={label}>
{label}
</DropdownTriggerLabel>
);
return (
<ThemeProvider theme={colorsTheme.dropdown}>
<DXCDropdownContainer margin={margin} size={size} disabled={disabled}>
<div
onMouseOver={expandOnHover && !disabled ? handleClickListItem : undefined}
onMouseOut={handleCloseOver}
onFocus={handleCloseOver}
onBlur={handleCloseOver}
>
<DropdownTrigger
opened={anchorEl === null ? false : true}
onClick={handleClickListItem}
disabled={disabled}
label={label}
caretHidden={caretHidden}
margin={margin}
size={size}
ref={ref}
tabIndex={tabIndex}
>
<DropdownTriggerContainer caretHidden={caretHidden}>
{iconPosition === "after" && labelComponent}
{icon && (
<ButtonIconContainer label={label} iconPosition={iconPosition} disabled={disabled}>
{typeof icon === "string" ? <ButtonIcon src={icon} /> : icon}
</ButtonIconContainer>
)}
{iconPosition === "before" && labelComponent}
</DropdownTriggerContainer>
<CaretIconContainer caretHidden={caretHidden} disabled={disabled}>
{!caretHidden && (anchorEl === null ? <DownArrowIcon /> : <UpArrowIcon />)}
</CaretIconContainer>
</DropdownTrigger>
<DXCMenu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
getContentAnchorEl={null}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
size={size}
width={width}
role={undefined}
transition
disablePortal
placement="bottom-start"
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={Boolean(anchorEl)} id="menu-list-grow">
{options.map((option) => (
<MenuItem
key={option.value}
value={option.value}
disableRipple={true}
onClick={(event) => handleMenuItemClick(option)}
>
{optionsIconPosition === "after" && <span className="optionLabel">{option.label}</span>}
{option.icon && (
<ListIconContainer label={option.label} iconPosition={optionsIconPosition}>
{typeof option.icon === "string" ? <ListIcon src={option.icon} /> : option.icon}
</ListIconContainer>
)}
{optionsIconPosition === "before" && <span className="optionLabel">{option.label}</span>}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</DXCMenu>
</div>
</DXCDropdownContainer>
</ThemeProvider>
);
}
Example #21
Source File: ClosablePopper.tsx From clearflask with Apache License 2.0 | 4 votes |
render() {
const {
children,
classes,
theme,
paperClassName,
zIndex,
onClose,
anchorType,
closeButtonPosition,
clickAway,
clickAwayProps,
arrow,
transitionCmpt,
transitionProps,
placement,
useBackdrop,
...popperProps
} = this.props;
const TransitionCmpt = transitionCmpt || Fade;
var anchorEl: PopperProps['anchorEl'];
if (this.props.anchorType === 'native') {
anchorEl = this.props.anchor;
} else {
// Overly complicated way to ensure popper.js
// always gets some kind of coordinates
anchorEl = () => {
var el: ReferenceObject | undefined | null;
if (!el && this.props.anchorType === 'ref') {
el = this.props.anchor.current;
}
if (!el && this.props.anchorType === 'element') {
el = (typeof this.props.anchor === 'function')
? this.props.anchor()
: this.props.anchor;
}
if (!el && this.props.anchorType === 'virtual') {
const virtualAnchor = this.props.anchor;
const bounds = virtualAnchor?.() || this.boundsLast;
if (!!bounds) {
this.boundsLast = bounds;
}
if (bounds) {
el = {
clientHeight: bounds.height,
clientWidth: bounds.width,
getBoundingClientRect: () => {
const boundsInner = virtualAnchor?.() || this.boundsLast || bounds;
this.boundsLast = boundsInner;
const domRect: MyDomRect = {
height: boundsInner.height,
width: boundsInner.width,
top: boundsInner.top,
bottom: boundsInner.top + boundsInner.height,
left: boundsInner.left,
right: boundsInner.left + boundsInner.width,
x: boundsInner.width >= 0 ? boundsInner.left : (boundsInner.left - boundsInner.width),
y: boundsInner.height >= 0 ? boundsInner.top : (boundsInner.top - boundsInner.height),
toJSON: () => domRect,
};
return domRect;
}
}
}
}
if (!el) {
el = this.anchorRef.current;
}
if (!el) {
const domRect: MyDomRect = {
height: 0,
width: 0,
top: 0,
bottom: 0,
left: 0,
right: 0,
x: 0,
y: 0,
toJSON: () => domRect,
};
el = {
clientHeight: 0,
clientWidth: 0,
getBoundingClientRect: () => domRect,
};
}
return el;
};
}
return (
<>
<div ref={this.anchorRef} />
<Popper
placement={placement || 'right-start'}
transition
{...popperProps}
anchorEl={anchorEl}
className={classNames(
classes.popper,
popperProps.className,
!popperProps.open && classes.hidden,
)}
modifiers={{
...(arrow ? {
arrow: {
enabled: true,
element: this.arrowRef.current || '[x-arrow]',
},
} : {}),
...(popperProps.modifiers || {}),
}}
>
{props => (
<ClickAwayListener
mouseEvent='onMouseDown'
onClickAway={() => clickAway && onClose()}
{...clickAwayProps}
>
<TransitionCmpt
{...props.TransitionProps}
{...transitionProps}
>
<Paper className={classNames(paperClassName, classes.paper)}>
{arrow && (
<span x-arrow='true' className={classes.arrow} ref={this.arrowRef} />
)}
{closeButtonPosition !== 'disable' && (
<IconButton
classes={{
label: classes.closeButtonLabel,
root: classNames(
classes.closeButton,
(closeButtonPosition === 'top-right' || !closeButtonPosition) && classes.closeButtonTopRight,
closeButtonPosition === 'top-left' && classes.closeButtonTopLeft,
),
}}
aria-label="Close"
onClick={() => onClose()}
>
<CloseIcon className={classes.closeIcon} fontSize='inherit' />
</IconButton>
)}
{children}
</Paper>
</TransitionCmpt>
</ClickAwayListener>
)}
</Popper>
</>
);
}
Example #22
Source File: SQFormDatePickerWithDateFNS.tsx From SQForm with MIT License | 4 votes |
function SQFormDatePickerWithDateFNS({
name,
label,
size = 'auto',
isDisabled = false,
placeholder = '',
onBlur,
onChange,
setDisabledDate,
muiFieldProps,
muiTextInputProps = {},
isCalendarOnly = false,
InputProps,
InputAdornmentProps,
}: SQFormDatePickerDateFNSProps): JSX.Element {
const {
formikField: {field, helpers},
fieldState: {isFieldError, isFieldRequired},
fieldHelpers: {handleBlur, HelperTextComponent},
} = useForm<Date | null, Date | null>({
name,
onBlur,
});
const handleChange = (date: Date | null) => {
helpers.setValue(date);
onChange && onChange(date);
};
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
const handleClose = () => setIsCalendarOpen(false);
const toggleCalendar = () => setIsCalendarOpen(!isCalendarOpen);
const handleClickAway = () => {
if (isCalendarOpen) {
setIsCalendarOpen(false);
}
};
const classes = useStyles();
// An empty string will not reset the DatePicker so we have to pass null
const value = field.value || null;
return (
<ClickAwayListener onClickAway={handleClickAway}>
<Grid item sm={size}>
<DatePicker
label={label}
disabled={isDisabled}
shouldDisableDate={setDisabledDate}
value={value}
onChange={handleChange}
onClose={handleClose}
onOpen={toggleCalendar}
open={isCalendarOpen}
renderInput={(inputProps) => {
return (
<TextField
{...inputProps}
name={name}
color="primary"
error={isFieldError}
fullWidth={true}
inputProps={{...inputProps.inputProps, ...muiTextInputProps}}
InputLabelProps={{shrink: true}}
FormHelperTextProps={{error: isFieldError}}
helperText={!isDisabled && HelperTextComponent}
placeholder={placeholder}
onBlur={handleBlur}
required={isFieldRequired}
onClick={
isCalendarOnly && !isDisabled
? toggleCalendar
: handleClickAway
}
classes={classes}
/>
);
}}
InputProps={InputProps}
InputAdornmentProps={InputAdornmentProps}
{...muiFieldProps}
/>
</Grid>
</ClickAwayListener>
);
}
Example #23
Source File: SQFormDatePicker.tsx From SQForm with MIT License | 4 votes |
function SQFormDatePicker({
name,
label,
size = 'auto',
isDisabled = false,
placeholder = '',
onBlur,
onChange,
setDisabledDate,
muiFieldProps,
muiTextInputProps = {},
isCalendarOnly = false,
InputProps,
InputAdornmentProps,
}: SQFormDatePickerProps): JSX.Element {
const {
formikField: {field, helpers},
fieldState: {isFieldError, isFieldRequired},
fieldHelpers: {handleBlur, HelperTextComponent},
} = useForm<Moment | null, Moment | null>({
name,
onBlur,
});
const handleChange = (date: Moment | null) => {
helpers.setValue(date);
onChange && onChange(date);
};
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
const handleClose = () => setIsCalendarOpen(false);
const toggleCalendar = () => setIsCalendarOpen(!isCalendarOpen);
const handleClickAway = () => {
if (isCalendarOpen) {
setIsCalendarOpen(false);
}
};
const classes = useStyles();
// An empty string will not reset the DatePicker so we have to pass null
const value = field.value || null;
return (
<ClickAwayListener onClickAway={handleClickAway}>
<Grid item sm={size}>
<DatePicker
label={label}
disabled={isDisabled}
shouldDisableDate={setDisabledDate}
value={value}
onChange={handleChange}
onClose={handleClose}
onOpen={toggleCalendar}
open={isCalendarOpen}
renderInput={(inputProps) => {
return (
<TextField
{...inputProps}
name={name}
color="primary"
error={isFieldError}
fullWidth={true}
inputProps={{...inputProps.inputProps, ...muiTextInputProps}}
InputLabelProps={{shrink: true}}
FormHelperTextProps={{error: isFieldError}}
helperText={!isDisabled && HelperTextComponent}
placeholder={placeholder}
onBlur={handleBlur}
required={isFieldRequired}
onClick={
isCalendarOnly && !isDisabled
? toggleCalendar
: handleClickAway
}
classes={classes}
/>
);
}}
InputProps={InputProps}
InputAdornmentProps={InputAdornmentProps}
{...muiFieldProps}
/>
</Grid>
</ClickAwayListener>
);
}