Filter treeNodes in Ant Design Tree - javascript

I'm using a Searchable Tree component in Ant Design. I would like to filter treeNodes on search. So that if a search value is found, a treeNode is shown. If not, it is hidden (i. e. filtered).
There is a filterTreeNode prop in API. Is it possible to filter treeNodes (hide unrelevant treeNodes from search) with this prop? If not, how can achieve the desired result?
Here is my code for filterTreeNode function:
const filterTreeNode = (node) => {
const title = node.title.props.children[2];
const result = node.key.indexOf(searchValue) !== -1 ? true : false
console.log(searchValue);
console.log(result);
return result;
};
I can see that the result (true or false) is logged in the console, but with the Tree itself nothing happens.
Here is a link to codesandbox and the full code for the component:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { Tree, Input } from "antd";
import gData from "./gData.js";
const { Search } = Input;
const dataList = [];
const generateList = (data) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { key } = node;
dataList.push({ key, title: key });
if (node.children) {
generateList(node.children);
}
}
};
generateList(gData);
const getParentKey = (key, tree) => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const SearchTree = () => {
const [expandedKeys, setExpandedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState("");
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const onChange = (e) => {
const { value } = e.target;
const expandedKeys = dataList
.map((item) => {
if (item.title.indexOf(value) > -1) {
return getParentKey(item.key, gData);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
if (value) {
setExpandedKeys(expandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
} else {
setExpandedKeys([]);
setSearchValue("");
setAutoExpandParent(false);
}
};
const filterTreeNode = (node) => {
const title = node.title.props.children[2];
const result = title.indexOf(searchValue) !== -1 ? true : false;
console.log(searchValue);
console.log(result);
return result;
};
const loop = (data) =>
data.map((item) => {
const index = item.title.indexOf(searchValue);
const beforeStr = item.title.substr(0, index);
const afterStr = item.title.substr(index + searchValue.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
) : (
<span>{item.title}</span>
);
if (item.children) {
return { title, key: item.key, children: loop(item.children) };
}
return {
title,
key: item.key
};
});
return (
<div>
<Search
style={{ marginBottom: 8 }}
placeholder="Search"
onChange={onChange}
/>
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={loop(gData)}
filterTreeNode={filterTreeNode}
/>
</div>
);
};
ReactDOM.render(<SearchTree />, document.getElementById("container"));

As mentioned in the API document, filterTreeNode will highlight tree node, and will not hide it.
filterTreeNode
Defines a function to filter (highlight) treeNodes. When the function returns true, the corresponding treeNode will be highlighted
If you want to hide tree node, you will have to manually filter it first before before passing it to Tree in loop function, something like:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Tree, Input } from "antd";
import gData from "./gData.js";
const { Search } = Input;
const dataList = [];
const generateList = (data) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { key } = node;
dataList.push({ key, title: key });
if (node.children) {
generateList(node.children);
}
}
};
generateList(gData);
const getParentKey = (key, tree) => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const SearchTree = () => {
const [expandedKeys, setExpandedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState("");
const [treeData, setTreeData] = useState(gData);
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const onChange = (e) => {
const value = e.target.value?.toLowerCase();
const expandedKeys = dataList
.map((item) => {
if (item.title.indexOf(value) > -1) {
return getParentKey(item.key, gData);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
if (value) {
const hasSearchTerm = (n) => n.toLowerCase().indexOf(value) !== -1;
const filterData = (arr) =>
arr?.filter(
(n) => hasSearchTerm(n.title) || filterData(n.children)?.length > 0
);
const filteredData = filterData(gData).map((n) => {
return {
...n,
children: filterData(n.children)
};
});
setTreeData(filteredData);
setExpandedKeys(expandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
} else {
setTreeData(gData);
setExpandedKeys([]);
setSearchValue("");
setAutoExpandParent(false);
}
};
const filterTreeNode = (node) => {
const title = node.title.props.children[2];
const result = title.indexOf(searchValue) !== -1 ? true : false;
console.log(searchValue);
console.log(result);
return result;
};
const loop = (data) =>
data.map((item) => {
const index = item.title.indexOf(searchValue);
const beforeStr = item.title.substr(0, index);
const afterStr = item.title.substr(index + searchValue.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
) : (
<span>{item.title}</span>
);
if (item.children) {
return { title, key: item.key, children: loop(item.children) };
}
return {
title,
key: item.key
};
});
return (
<div>
<Search
style={{ marginBottom: 8 }}
placeholder="Search"
onChange={onChange}
/>
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={loop(treeData)}
filterTreeNode={filterTreeNode}
/>
</div>
);
};
ReactDOM.render(<SearchTree />, document.getElementById("container"));

Couple weeks ago, i had to meet same like this client's enhancement. So how i did it was, we had Search over DirectoryTree and inside tree we rendered the nodes with a function
<DirectoryTree
onExpand={handleOperatorExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onSelect={handleOperatorSelect}
>
{renderTreeNodes(mtoCharts)}
</DirectoryTree>
In search onChange event we saved values to a state, like searchValue. Then searchValue was checked in renderTreeNodes function with regex:
let dynamicRegex = `.*(${searchValue.toUpperCase()}){1}.*`;
if
item.name.match(dynamicRegex)
then we display the node otherwise we dont. SIMPLE !
UPDATE : codes
<Search onChange={handleOperatorChange} />
const handleOperatorChange = e => {
const { value } = e.target;
let temp = value.replace(/[\.,-\/#!$%\^&\*;:{}=\-_`~()"'+#<>?\\\[\]]/g, '');
setSearchValue(temp);
setAutoExpandParent(true);};
and finally renderTreeNodes :
const renderTreeNodes = data =>
data &&
data instanceof Array &&
data.map(item => {
const title = (
<Highlighter
highlightStyle={{ color: '#f50' }}
searchWords={[searchValue]}
autoEscape={true}
textToHighlight={item.name}
/>
);
if (searchValue) {
let dynamicRegex = `.*(${searchValue.toUpperCase()}){1}.*`;
if (!isEmpty(item.children)) {
// let doesChildrenHaveMatchingName = false;
// item.children.forEach(itemChild => {
// if (itemChild.name.match(dynamicRegex)) {
// doesChildrenHaveMatchingName = true;
// }
// });
// if (doesChildrenHaveMatchingName) {
return (
<TreeNode
className={!item.active ? 'dim-category' : ''}
key={item.code}
title={title}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
)}
>
{renderTreeNodes(item.children)}
</TreeNode>
);
// }
}
if (item.name.match(dynamicRegex) || item.path.match(dynamicRegex)) {
return (
<TreeNode
className={item.active ? 'false' : 'dim-category'}
key={item.code}
title={!item.leaf ? title : getMtoTitle(item, title)}
{...(item.leaf ? { leaf: item.leaf } : {})}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => {
return item.leaf ? (
<Icon type="money-collect" style={{ color: '#47bfa5' }} />
) : (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
);
}}
/>
);
}
} else {
if (!isEmpty(item.children)) {
return (
<TreeNode
className={!item.active ? 'dim-category' : ''}
key={item.code}
title={title}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
)}
>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return (
<TreeNode
className={item.active ? 'false' : 'dim-category'}
key={item.code}
title={!item.leaf ? title : getMtoTitle(item, title)}
{...(item.leaf ? { leaf: item.leaf } : {})}
id={item.id}
code={item.code}
level={item.level}
parentId={item.parentId}
parentReference={item.path}
icon={({ expanded }) => {
return item.leaf ? (
<Icon type="money-collect" style={{ color: '#47bfa5' }} />
) : (
<Icon type={expanded ? 'folder-open' : 'folder'} style={{ color: '#32327f' }} />
);
}}
/>
);
}
});

Related

Cannot get synced state in React INK when using "useStdin" hook with rawMode

I'm using ink package, and I'm building the following component:
MultiSelect.tsx file:
import React, { useEffect, useState } from 'react';
import { useStdin } from 'ink';
import type { ISelectItem, ISelectItemSelection } from './interfaces/select-item';
import { ARROW_DOWN, ARROW_UP, ENTER, SPACE } from './constants/input';
import MultiSelectView from './MultiSelect.view';
interface IProps {
readonly items: ISelectItem[];
readonly onSubmit: (selectedItems: string[]) => void;
}
const MultiSelect: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
const { stdin, setRawMode } = useStdin();
const [itemsSelectionState, setItemsSelectionState] = useState<ISelectItemSelection[]>(
props.items.map((item, index) => ({ ...item, selected: false, isHighlighted: index === 0 })),
);
const stdinInputHandler = (data: unknown) => {
const rawData = String(data);
if (rawData === ARROW_DOWN) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === prev.length - 1) {
clonedPrev[0]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex + 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === ARROW_UP) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === 0) {
clonedPrev[prev.length - 1]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex - 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === SPACE) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.selected = !prev[highlightedIndex]!.selected;
return clonedPrev;
});
}
if (rawData === ENTER) {
const selectedItemsValues = itemsSelectionState
.filter((item) => item.selected)
.map((item) => item.value);
props.onSubmit(selectedItemsValues);
}
};
useEffect(() => {
setRawMode(true);
stdin?.on('data', stdinInputHandler);
return () => {
stdin?.removeListener('data', stdinInputHandler);
setRawMode(false);
};
}, []);
return <MultiSelectView itemsSelection={itemsSelectionState} />;
};
export default MultiSelect;
MultiSelect.view.tsx file:
import { Box, Text } from 'ink';
import React from 'react';
import type { ISelectItemSelection } from './interfaces/select-item';
interface IProps {
readonly itemsSelection: ISelectItemSelection[];
}
const MultiSelectView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
return (
<Box display="flex" flexDirection="column">
{props.itemsSelection.map((item) => (
<Box key={item.value} display="flex" flexDirection="row">
<Text color="blue">{item.isHighlighted ? '❯' : ' '}</Text>
<Text color="magenta">
{item.selected ? '◉' : '◯'}
</Text>
<Text color="white" bold={item.selected}>
{item.label}
</Text>
</Box>
))}
</Box>
);
};
export default MultiSelectView;
Then, when I use this component in my code:
const onSubmitItems = (items: string[]) => {
console.log(items);
};
render(<MultiSelect items={items} onSubmit={onSubmitItems} />);
the render function is imported from ink and items is something like [{value: 'x', label: 'x'}, {value:'y', label:'y'}]
When I hit the enter key, Then onSubmitItems is triggered, but it outputs empty list, although I did select some items...
For example, in this output:
I picked 3 items, but output is still empty list. And it does seem like the state changes, so why I don't get the updated state?
You can resolve the issue by using useCallback:
const stdinInputHandler = useCallback((data: Buffer) => {
const rawData = String(data);
if (rawData === ARROW_DOWN) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === prev.length - 1) {
clonedPrev[0]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex + 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === ARROW_UP) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === 0) {
clonedPrev[prev.length - 1]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex - 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === SPACE) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.selected = !prev[highlightedIndex]!.selected;
return clonedPrev;
});
}
if (rawData === ENTER) {
const selectedItemsValues = itemsSelectionState
.filter((item) => item.selected)
.map((item) => item.value);
props.onSubmit(selectedItemsValues);
}
}, [itemsSelectionState]);
Then, in your useEffect:
useEffect(() => {
setRawMode(true);
stdin?.on('data', stdinInputHandler);
return () => {
stdin?.removeListener('data', stdinInputHandler);
setRawMode(false);
};
}, [stdinInputHandler]);
now useEffect will re-register the input handler with new functions locals up-to-date with the state

How to use useDraggable / useSortable hooks of dnd-kit library properly?

I'm trying to create a simple calculator with React and dnd-kit. Elements of calculator can be dragged to droppable area and can be sorted inside of it. You can see a problem on the gif: when I drag element from left side to droppable area, there is no animation of dragging but element can be dropped to area. And inside of droppable area elements can be beautifly sorted with animation of dragging.
So, I need drag animation to work when I drag elements from left side to droppable area.
Code for App component:
const App: FC = () => {
const [selected, setSelected] = useState('Constructor')
const [droppedElems, setDroppedElems] = useState<CalcElemListInterface[]>([])
const handleActiveSwitcher = (id: string) => {
setSelected(id)
}
const deleteDroppedElem = (item: CalcElemListInterface) => {
const filtered = [...droppedElems].filter(elem => elem.id !== item.id)
setDroppedElems(filtered)
}
const leftFieldStyles = cn(styles.left, {
[styles.hidden]: selected === 'Runtime'
})
const calcElementsList = calcElemListArray.map((item) => {
const index = droppedElems.findIndex(elem => elem.id === item.id)
const layoutDisabledStyle = index !== -1
return (
<CalcElemLayout
key={item.id}
id={item.id}
item={item}
layoutDisabledStyle={layoutDisabledStyle}
/>
)
})
const handleDragEnd = (event: DragEndEvent) => {
const { id, list } = event.active.data.current as CalcElemListInterface
const elem = {id, list}
if (event.over && event.over.id === 'droppable') {
setDroppedElems((prev) => {
return [...prev, elem]
})
}
}
return (
<div className={styles.layout}>
<div className={styles.top}>
<Switcher
selected={selected}
handleActiveSwitcher={handleActiveSwitcher}
/>
</div>
<DndContext
onDragEnd={handleDragEnd}
>
<div className={styles.content}>
<div className={leftFieldStyles}>
{calcElementsList}
</div>
<DropElemLayout
deleteDroppedElem={deleteDroppedElem}
selected={selected}
droppedElems={droppedElems}
setDroppedElems={setDroppedElems}
/>
</div>
</DndContext>
</div>
)
}
Code for droppable area:
const DropElemLayout: FC<DropElemLayoutInterface> = ({ selected, droppedElems, deleteDroppedElem, setDroppedElems }) => {
const { isOver, setNodeRef } = useDroppable({
id: 'droppable'
})
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const style = {
backgroundColor: (isOver && !droppedElems.length) ? '#F0F9FF' : undefined,
}
const droppedRuntimeElemList = droppedElems.map((item) => {
const layoutEnabledStyle = droppedElems.length ? true : false
return (
<CalcElemLayout
key={item.id}
id={item.id}
item={item}
deleteDroppedElem={deleteDroppedElem}
selected={selected}
layoutEnabledStyle={layoutEnabledStyle}
/>
)
})
const droppedElemList = !droppedElems.length
?
<div className={styles.rightContent}>
<Icon name="#drop"/>
<p>Перетащите сюда</p>
<span>любой элемент</span>
<span>из левой панели</span>
</div>
:
droppedRuntimeElemList
const className = !droppedElems.length ? styles.right : styles.left
const handleDragEnd = (event: DragEndEvent) => {
if (event.active.id !== event.over?.id) {
setDroppedElems((items: CalcElemListInterface[]) => {
const oldIndex = items.findIndex(item => item.id === event.active?.id)
const newIndex = items.findIndex(item => item.id === event.over?.id)
return arrayMove(items, oldIndex, newIndex)
})
}
}
return (
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
>
<div
ref={setNodeRef}
className={className}
style={style}
>
<SortableContext
items={droppedElems}
strategy={verticalListSortingStrategy}
>
{droppedElemList}
</SortableContext>
</div>
</DndContext>
)
}
Code for Element itself:
const CalcElemLayout: FC<CalcElemLayoutInterface> = ({ item, id, deleteDroppedElem, selected, layoutDisabledStyle, layoutEnabledStyle }) => {
const { current } = useAppSelector(state => state.calculator)
// const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
// id: id,
// data: {...item},
// disabled: selected === 'Runtime'
// })
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: id,
data: {...item},
disabled: selected === 'Runtime'
})
const style = {
transform: CSS.Translate.toString(transform),
transition: transition
}
const handleDeleteDroppedElem = () => {
deleteDroppedElem?.(item)
}
const doubleClickCondition = selected === 'Constructor' ? handleDeleteDroppedElem : undefined
const layoutStyle = cn(styles.elemLayout, {
[styles.operators]: item.id === 'operators',
[styles.digits]: item.id === 'digits',
[styles.equal]: item.id === 'equal',
[styles.disabled]: layoutDisabledStyle,
[styles.enabled]: layoutEnabledStyle,
})
const buttonList = item.list?.map(elem => (
<Button
key={elem.name}
elem={elem.name}
selected={selected!}
/>
))
const resultStyle = cn(styles.result, {
[styles.minified]: current.length >= 10
})
const elemList = item.id === 'result'
?
<div className={resultStyle}>{current}</div>
:
buttonList
const overlayStyle = {
opacity: '0.5',
}
return (
<>
<div
ref={setNodeRef}
className={layoutStyle}
onDoubleClick={doubleClickCondition}
style={style}
{...attributes}
{...listeners}
>
{elemList}
</div>
</>
)
}
All you need to do is to add DragOverlay component properly in Element like so:
const CalcElemLayout: FC<CalcElemLayoutInterface> = ({ item, id, deleteDroppedElem, selected, layoutDisabledStyle, layoutEnabledStyle }) => {
const { current } = useAppSelector(state => state.calculator)
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: id,
data: {...item},
disabled: selected === 'Runtime',
})
const handleDeleteDroppedElem = () => {
deleteDroppedElem?.(item)
}
const doubleClickCondition = selected === 'Constructor' ? handleDeleteDroppedElem : undefined
const layoutStyle = cn(styles.elemLayout, {
[styles.operators]: item.id === 'operators',
[styles.digits]: item.id === 'digits',
[styles.equal]: item.id === 'equal',
[styles.disabled]: layoutDisabledStyle,
[styles.enabled]: layoutEnabledStyle,
[styles.transparent]: isDragging
})
const style = {
transform: CSS.Translate.toString(transform),
transition: transition
}
const buttonList = item.list?.map(elem => (
<Button
key={elem.name}
elem={elem.name}
selected={selected!}
/>
))
const resultStyle = cn(styles.result, {
[styles.minified]: current.length >= 10
})
const elemList = item.id === 'result'
?
<div className={resultStyle}>{current}</div>
:
buttonList
const dragOverlayContent = isDragging
?
<div
className={layoutStyle}
style={{
opacity: isDragging ? '1' : '',
boxShadow: isDragging ? '0px 2px 4px rgba(0, 0, 0, 0.06), 0px 4px 6px rgba(0, 0, 0, 0.1)' : ''
}}
>
{elemList}
</div>
:
null
return (
<>
<div
ref={setNodeRef}
className={layoutStyle}
onDoubleClick={doubleClickCondition}
style={style}
{...attributes}
{...listeners}
>
{elemList}
</div>
<DragOverlay dropAnimation={null}>
{dragOverlayContent}
</DragOverlay>
</>
)
}

React has detected a change in the order of Hooks called by AddLiquidity

That was the problem with the error I got in the console.
The function is called on form submission. at first, it is working as expected but when calling this function on form submit. I get this following error.index.js:1 Warning: React has detected a change in the order of Hooks called by null. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks:
import React, { useCallback, useState } from 'react'
import { BigNumber } from '#ethersproject/bignumber'
import { TransactionResponse } from '#ethersproject/providers'
import { Currency, currencyEquals, ETHER, TokenAmount, WETH } from '#pancakeswap-libs/sdk'
import { Button, CardBody, AddIcon, Text as UIKitText } from '#pancakeswap-libs/uikit'
import { RouteComponentProps } from 'react-router-dom'
import { LightCard } from 'components/Card'
import { AutoColumn, ColumnCenter } from 'components/Column'
import TransactionConfirmationModal, { ConfirmationModalContent } from 'components/TransactionConfirmationModal'
import CardNav from 'components/CardNav'
import CurrencyInputPanel from 'components/CurrencyInputPanel'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import { AddRemoveTabs } from 'components/NavigationTabs'
import { MinimalPositionCard } from 'components/PositionCard'
import Row, { RowBetween, RowFlat } from 'components/Row'
import { PairState } from 'data/Reserves'
import { useActiveWeb3React } from 'hooks'
import { useCurrency } from 'hooks/Tokens'
import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback'
import { Field } from 'state/mint/actions'
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from 'state/mint/hooks'
import { useTransactionAdder } from 'state/transactions/hooks'
import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from 'state/user/hooks'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from 'utils'
import { maxAmountSpend } from 'utils/maxAmountSpend'
import { wrappedCurrency } from 'utils/wrappedCurrency'
import { currencyId } from 'utils/currencyId'
import Pane from 'components/Pane'
import ConnectWalletButton from 'components/ConnectWalletButton'
import useI18n from 'hooks/useI18n'
import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds'
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
import { PoolPriceBar } from './PoolPriceBar'
import { ROUTER_ADDRESS } from '../../constants'
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB },
},
history,
}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string }>) {
const { account, chainId, library } = useActiveWeb3React()
const currencyA = useCurrency(currencyIdA)
const currencyB = useCurrency(currencyIdB)
const TranslateString = useI18n()
const oneCurrencyIsWBNB = Boolean(
chainId &&
((currencyA && currencyEquals(currencyA, WETH[chainId])) ||
(currencyB && currencyEquals(currencyB, WETH[chainId])))
)
const expertMode = useIsExpertMode()
// mint state
const { independentField, typedValue, otherTypedValue } = useMintState()
const {
dependentField,
currencies,
pair,
pairState,
currencyBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error,
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
// txn values
const [deadline] = useUserDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const [txHash, setTxHash] = useState<string>('')
// get formatted amounts
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: noLiquidity ? otherTypedValue : parsedAmounts[dependentField]?.toSignificant(6) ?? '',
}
// get the max amounts user can add
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
[field]: maxAmountSpend(currencyBalances[field]),
}
},
{}
)
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
[field]: maxAmounts[field]?.equalTo(parsedAmounts[field] ?? '0'),
}
},
{}
)
// check whether the user has approved the router on the tokens
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_A], ROUTER_ADDRESS)
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_B], ROUTER_ADDRESS)
const addTransaction = useTransactionAdder()
async function onAdd() {
if (!chainId || !library || !account) return
const router = getRouterContract(chainId, library, account)
const { [Field.CURRENCY_A]: parsedAmountA, [Field.CURRENCY_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !currencyA || !currencyB) {
return
}
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0],
}
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
let estimate
let method: (...args: any) => Promise<TransactionResponse>
let args: Array<string | string[] | number>
let value: BigNumber | null
if (currencyA === ETHER || currencyB === ETHER) {
const tokenBIsBNB = currencyB === ETHER
estimate = router.estimateGas.addLiquidityETH
method = router.addLiquidityETH
args = [
wrappedCurrency(tokenBIsBNB ? currencyA : currencyB, chainId)?.address ?? '', // token
(tokenBIsBNB ? parsedAmountA : parsedAmountB).raw.toString(), // token desired
amountsMin[tokenBIsBNB ? Field.CURRENCY_A : Field.CURRENCY_B].toString(), // token min
amountsMin[tokenBIsBNB ? Field.CURRENCY_B : Field.CURRENCY_A].toString(), // eth min
account,
deadlineFromNow,
]
value = BigNumber.from((tokenBIsBNB ? parsedAmountB : parsedAmountA).raw.toString())
} else {
estimate = router.estimateGas.addLiquidity
method = router.addLiquidity
args = [
wrappedCurrency(currencyA, chainId)?.address ?? '',
wrappedCurrency(currencyB, chainId)?.address ?? '',
parsedAmountA.raw.toString(),
parsedAmountB.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
value = null
}
setAttemptingTxn(true)
// const aa = await estimate(...args, value ? { value } : {})
await estimate(...args, value ? { value } : {})
.then((estimatedGasLimit) =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit),
}).then((response) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Add ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${currencies[Field.CURRENCY_A]?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencies[Field.CURRENCY_B]?.symbol}`,
})
setTxHash(response.hash)
})
)
.catch((e) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (e?.code !== 4001) {
console.error(e)
}
})
}
const modalHeader = () => {
return noLiquidity ? (
<AutoColumn gap="20px">
<LightCard mt="20px" borderRadius="20px">
<RowFlat>
<UIKitText fontSize="48px" mr="8px">
{`${currencies[Field.CURRENCY_A]?.symbol}/${currencies[Field.CURRENCY_B]?.symbol}`}
</UIKitText>
<DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat>
</LightCard>
</AutoColumn>
) : (
<AutoColumn gap="20px">
<RowFlat style={{ marginTop: '20px' }}>
<UIKitText fontSize="48px" mr="8px">
{liquidityMinted?.toSignificant(6)}
</UIKitText>
<DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat>
<Row>
<UIKitText fontSize="24px">
{`${currencies[Field.CURRENCY_A]?.symbol}/${currencies[Field.CURRENCY_B]?.symbol} Pool Tokens`}
</UIKitText>
</Row>
<UIKitText small textAlign="left" padding="8px 0 0 0 " style={{ fontStyle: 'italic' }}>
{`Output is estimated. If the price changes by more than ${allowedSlippage / 100
}% your transaction will revert.`}
</UIKitText>
</AutoColumn>
)
}
const modalBottom = () => {
return (
<ConfirmAddModalBottom
price={price}
currencies={currencies}
parsedAmounts={parsedAmounts}
noLiquidity={noLiquidity}
onAdd={onAdd}
poolTokenPercentage={poolTokenPercentage}
/>
)
}
const pendingText = `Supplying ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${currencies[Field.CURRENCY_A]?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencies[Field.CURRENCY_B]?.symbol}`
const handleCurrencyASelect = useCallback(
(currA: Currency) => {
const newCurrencyIdA = currencyId(currA)
if (newCurrencyIdA === currencyIdB) {
history.push(`/add/${currencyIdB}/${currencyIdA}`)
} else {
history.push(`/add/${newCurrencyIdA}/${currencyIdB}`)
}
},
[currencyIdB, history, currencyIdA]
)
const handleCurrencyBSelect = useCallback(
(currB: Currency) => {
const newCurrencyIdB = currencyId(currB)
if (currencyIdA === newCurrencyIdB) {
if (currencyIdB) {
history.push(`/add/${currencyIdB}/${newCurrencyIdB}`)
} else {
history.push(`/add/${newCurrencyIdB}`)
}
} else {
history.push(`/add/${currencyIdA || 'BNB'}/${newCurrencyIdB}`)
}
},
[currencyIdA, history, currencyIdB]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onFieldAInput('')
}
setTxHash('')
}, [onFieldAInput, txHash])
return (
<>
<CardNav activeIndex={1} />
<AppBody>
<AddRemoveTabs adding />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash}
content={() => (
<ConfirmationModalContent
title={
noLiquidity
? TranslateString(1154, 'You are creating a pool')
: TranslateString(1156, 'You will receive')
}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<CardBody>
<AutoColumn gap="20px">
{noLiquidity && (
<ColumnCenter>
<Pane>
<AutoColumn gap="12px">
<UIKitText>{TranslateString(1158, 'You are the first liquidity provider.')}</UIKitText>
<UIKitText>
{TranslateString(1160, 'The ratio of tokens you add will set the price of this pool.')}
</UIKitText>
<UIKitText>
{TranslateString(1162, 'Once you are happy with the rate click supply to review.')}
</UIKitText>
</AutoColumn>
</Pane>
</ColumnCenter>
)}
<CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
onMax={() => {
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
onCurrencySelect={handleCurrencyASelect}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
showCommonBases={false}
/>
<ColumnCenter>
<AddIcon color="textSubtle" />
</ColumnCenter>
<CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onFieldBInput}
onCurrencySelect={handleCurrencyBSelect}
onMax={() => {
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases={false}
/>
{currencies[Field.CURRENCY_A] && currencies[Field.CURRENCY_B] && pairState !== PairState.INVALID && (
<div>
<UIKitText
style={{ textTransform: 'uppercase', fontWeight: 600 }}
color="textSubtle"
fontSize="12px"
mb="2px"
>
{noLiquidity
? TranslateString(1164, 'Initial prices and pool share')
: TranslateString(1166, 'Prices and pool share')}
</UIKitText>
<Pane>
<PoolPriceBar
currencies={currencies}
poolTokenPercentage={poolTokenPercentage}
noLiquidity={noLiquidity}
price={price}
/>
</Pane>
</div>
)}
{!account ? (
<ConnectWalletButton width="100%" />
) : (
<AutoColumn gap="md">
{(approvalA === ApprovalState.NOT_APPROVED ||
approvalA === ApprovalState.PENDING ||
approvalB === ApprovalState.NOT_APPROVED ||
approvalB === ApprovalState.PENDING) &&
isValid && (
<RowBetween>
{approvalA !== ApprovalState.APPROVED && (
<Button
onClick={approveACallback}
disabled={approvalA === ApprovalState.PENDING}
style={{ width: approvalB !== ApprovalState.APPROVED ? '48%' : '100%' }}
>
{approvalA === ApprovalState.PENDING ? (
<Dots>Approving {currencies[Field.CURRENCY_A]?.symbol}</Dots>
) : (
`Approve ${currencies[Field.CURRENCY_A]?.symbol}`
)}
</Button>
)}
{approvalB !== ApprovalState.APPROVED && (
<Button
onClick={approveBCallback}
disabled={approvalB === ApprovalState.PENDING}
style={{ width: approvalA !== ApprovalState.APPROVED ? '48%' : '100%' }}
>
{approvalB === ApprovalState.PENDING ? (
<Dots>Approving {currencies[Field.CURRENCY_B]?.symbol}</Dots>
) : (
`Approve ${currencies[Field.CURRENCY_B]?.symbol}`
)}
</Button>
)}
</RowBetween>
)}
<Button
onClick={() => {
if (expertMode) {
onAdd()
} else {
setShowConfirm(true)
}
}}
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
width="100%"
>
{error ?? 'Supply'}
</Button>
</AutoColumn>
)}
</AutoColumn>
</CardBody>
</Wrapper>
</AppBody>
{pair && !noLiquidity && pairState !== PairState.INVALID ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWBNB} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
I am not sure on how this should be done. Any ideas on what I could do?
Thanks

Change all buttons backgroundColor made in react

Im doing a function(clearGame) to change all backgroundColor of the numbers i get in "NumbersParent". I tried using querySelector but all i get is "null" from changeStyle.
const newBet: React.FC = () => {
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
for (let i = 0; i <= spliceRangeJSON; i++) {
const changeStyle = document.querySelector<HTMLElement>('data-key');
changeStyle!.style.backgroundColor = '#ADC0C4';
}
};
const NumbersParent = (props: any) => {
const [numbersColor, setNumbersColor] = useState('#ADC0C4');
const changeButtonColor = () => {
if (numbersColor === '#ADC0C4') {
setNumbersColor(gamesJson[whichLoteriaIsVar].color);
} else {
setNumbersColor('#ADC0C4');
}
};
return (
<Numbers
color={numbersColor}
onClick={changeButtonColor}
>
{props.children}
</Numbers>
);
};
} return(
<NumbersContainer>
{numbersList.map((num) => (
<NumbersParent key={num} id={num} data-key={num}>
{num}
</NumbersParent>
))}
</NumbersContainer>
<Button onClick={clearGame}>Clear Game</Button
)

Onclick method in React is effecting every list item in a table, should only affect the one which is clicked

I have a tsx file which creates a table of comments. When the page is rendered, an array of information representing comments is gathered. A Set containing the indexes of the comments in the array, which is part of the state, determines whether a link under the comment reads 'show more' or 'show less'. Once that link is clicked, the index of the comment is added to the Set, and the state is updated with the addition of that index to the Set, if the Set contains the index of a comment when the state is updated, it should read 'show less'. The problem is when I click that link, it changes the link of each list element in the table, not just one.
import * as React from "react";
import { LocalizationInfo } from "emt-localization";
import { DateHandler } from "../handlers/date-handler";
import { UserIcon } from "./user-icon";
import { OnDownloadDocument } from "../models/generic-types";
import { getUserString } from "../models/user";
import { ClaimAction, ActionDetails, ReasonActionDetails, CommentDetails, DocumentDetails } from "../models/action";
const stateChangeActions = new Set<string>([
'Dispute',
'Rejection',
'SetUnderReview',
'Approval',
'Recall',
'Submission',
'RequestPartnerAction'
]);
const fiveMinutes = 5 * 60 * 1000;
const underscoreRegex = /_[^_]*_/g;
const actionTypeClassMap: {[actionType: string]: string} = {
'Submission': 'success',
'RequestPartnerAction': 'warn',
'Rejection': 'warn',
'Approval': 'success',
'Recall': 'warn',
'Dispute': 'warn',
'SetUnderReview': 'info',
'CustomerConsentDeclined': 'rejected'
};
const maxShortLength = 100;
interface ActionsProps {
readonly localization: LocalizationInfo;
readonly actions: ClaimAction[];
readonly onDownloadDocument: OnDownloadDocument;
readonly isInternalFacing: boolean;
readonly isHistoryPaneDisplay: boolean;
}
class ActionsState {
readonly expandedComments = new Set<number>();
}
export class Actions extends React.Component<ActionsProps, ActionsState> {
constructor(props: ActionsProps) {
super(props);
this.state = new ActionsState();
}
render():JSX.Element {
const loc = this.props.localization;
const isInternalFacing = this.props.isInternalFacing;
const toTime = (action:ClaimAction) => new Date(action.timeStamp).getTime();
const sortBy = (a:ClaimAction, b:ClaimAction) => toTime(b) - toTime(a);
const actions = this.props.actions.filter(action => {
if (isDocumentDetails(action.details)) {
if (action.actionType == 'DocumentSubmission' && action.details.documentType == 'Invoice') {
return false;
}
}
return true;
});
actions.sort(sortBy);
const grouped = groupActions(actions);
return (
<ul className={`claim-actions ${this.props.isHistoryPaneDisplay ? '' : 'user-comment-box'}`}>
{grouped.map((actions, index) => {
let actionClass = '';
actions.forEach(action => {
actionClass = actionClass || actionTypeClassMap[action.actionType];
});
const first = actions[0];
const icon = actionClass == 'success' ?
sequenceIcon('complete') :
actionClass == 'warn' ?
sequenceIcon('action-required') :
actionClass == 'rejected' ?
sequenceIcon('rejected') :
actionClass == 'info' ?
sequenceIcon('editing') :
<UserIcon
user={first.user}
isInternalFacing={isInternalFacing}
/>;
const elements = actions.map((action, actionIndex) => this.renderAction(action, actionIndex));
return (
<li className={actionClass} key={index}>
{icon}
<div className="win-color-fg-secondary">
<span className="claim-action-name win-color-fg-primary">
{ getUserString(first.user, isInternalFacing) }
</span>
<span className="text-caption">
{loc.piece("HistoryItemTitle", 0)}
{DateHandler.friendlyDate(first.timeStamp, true)}
</span>
</div>
{elements}
</li>
)
})}
</ul>
)
}
private renderAction(action:ClaimAction, actionIndex:number):JSX.Element|null {
const strings = this.props.localization.strings;
if (action.actionType == 'AddComments' || action.actionType == 'AddInternalComments') {
return this.renderComment((action.details as CommentDetails).comments, actionIndex);
}
const document = isDocumentDetails(action.details) ? action.details : null;
const documentLink = document ?
(key:number) =>
<a
key={key}
onClick={() => this.props.onDownloadDocument(document.documentId, document.name)}>
{document.name}
</a>
: null;
const locKey = `History_${action.actionType}`;
const localizedFlat = strings[locKey] || "";
const localized = replaceUnderscores(localizedFlat, documentLink);
const reason = (action.actionType === 'RequestPartnerAction' || action.actionType === 'Rejection') && action.details ? (action.details as ReasonActionDetails).reasonCode : '';
const reasonString = reason.charAt(0).toUpperCase() + reason.slice(1)
if (localized) {
return (
<div key={actionIndex}>
<div className="claim-action">
<span className="text-caption">{localized}</span>
</div>
<div className="claim-action">
{ reasonString && <span className="text-caption"><strong>{strings['ReasonLabel']}</strong>{` ${strings[reasonString]}`}</span> }
</div>
</div>
);
}
console.error(`Unknown action type ${action.actionType}`);
return null;
}
private renderComment(comment: string, actionIndex: number): JSX.Element {
const strings = this.props.localization.strings;
const canShorten = comment.length > maxShortLength;
const shouldShorten = canShorten && !this.state.expandedComments.has(actionIndex);
const shortened = shouldShorten ?
comment.substring(0, maxShortLength) + "\u2026" :
comment;
const paragraphs = shortened
.split('\n')
.map(s => s.trim())
.filter(s => s);
const elements = paragraphs.map((comment, i) =>
<div className="claim-comment" key={i}>
{comment}
</div>
);
const toggle = () => {
const next = new Set<number>(this.state.expandedComments);
if (next.has(actionIndex)) {
next.delete(actionIndex)
}
else {
next.add(actionIndex);
}
this.setState({ expandedComments: next });
}
const makeLink = (locKey:string) =>
<a
onClick={toggle}>{strings[locKey]}</a>;
const afterLink = canShorten ?
shouldShorten ?
makeLink('ShowMore') :
makeLink('ShowLess') :
null;
return (
<React.Fragment key={actionIndex}>
{elements}
{afterLink}
</React.Fragment>
);
}
}
// Function groups actions together under some conditions
function groupActions(actions:ClaimAction[]):ClaimAction[][] {
const grouped:ClaimAction[][] = [];
actions.forEach(action => {
if (grouped.length) {
const lastGroup = grouped[grouped.length - 1];
const timeDifference = new Date(lastGroup[0].timeStamp).getTime() - new Date(action.timeStamp).getTime();
if (stateChangeActions.has(lastGroup[0].actionType) && action.actionType == 'AddComments' && timeDifference < fiveMinutes) {
lastGroup.push(action);
return;
}
}
grouped.push([action]);
});
return grouped;
}
function isDocumentDetails(details:ActionDetails|null): details is DocumentDetails {
return !!details && (details.$concreteClass == 'InvoiceActionDetails' || details.$concreteClass == 'DocumentActionDetails');
}
function sequenceIcon(className: string):JSX.Element {
return (
<div className="sequence sequence-status claims-icon">
<div className={`step ${className}`} />
</div>
);
}
function replaceUnderscores(str: string, documentLink: ((k:number)=>JSX.Element)|null, startKey: number=0):JSX.Element[] {
if (!str) {
return [];
}
const match = underscoreRegex.exec(str);
if (!match) {
return replaceDocumentLink(str, documentLink, startKey);
}
const firstText = str.substring(0, match.index);
const middleText = match[0].substring(1, match[0].length - 1);
const lastText = str.substring(match.index + match[0].length);
const first = replaceUnderscores(firstText, documentLink, startKey);
const middle = [<strong className={'claims-emphasis'} key={startKey + first.length}>{middleText}</strong>];
const last = replaceUnderscores(lastText, documentLink, startKey + first.length + 1);
return first.concat(middle, last);
}
function replaceDocumentLink(str: string, documentLink: ((k:number)=>JSX.Element)|null, startKey: number=0):JSX.Element[] {
const replaceIndex = str.indexOf('{0}');
if (replaceIndex >= 0 && documentLink) {
return [
<React.Fragment key={startKey}>{str.substring(0, replaceIndex)}</React.Fragment>,
documentLink(startKey+1),
<React.Fragment key={startKey + 2}>{str.substring(replaceIndex+3)}</React.Fragment>
];
}
return [<React.Fragment key={startKey}>{str}</React.Fragment>];
}

Categories