/
LinkHandler.tsx
142 lines (123 loc) · 3.73 KB
/
LinkHandler.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Properties
*/
import * as React from "react";
import { BentleyError, BentleyStatus } from "@itwin/core-bentley";
import type { LinkElementsInfo } from "@itwin/appui-abstract";
import { UnderlinedButton } from "@itwin/core-react";
/** Render a single anchor tag */
function renderTag(
text: string,
links: LinkElementsInfo,
highlight?: (text: string) => React.ReactNode
) {
return (
<UnderlinedButton
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
links.onClick(text);
}}
>
{highlight ? highlight(text) : text}
</UnderlinedButton>
);
}
interface Match {
start: number;
end: number;
}
function matchComparison(matchA: Match, matchB: Match) {
if (matchA.start > matchB.start) return 1;
if (matchB.start > matchA.start) return -1;
return 0;
}
function renderTextPart(
text: string,
highlight?: (text: string) => React.ReactNode
): React.ReactNode {
return highlight ? highlight(text) : text;
}
function renderText(
text: string,
links: LinkElementsInfo,
highlight?: (text: string) => React.ReactNode
): React.ReactNode {
const { matcher } = links;
if (!matcher) return renderTag(text, links, highlight);
const matches = matcher(text);
// Sort just to be sure
matches.sort(matchComparison);
const parts: React.ReactNode[] = [];
let lastIndex = 0;
for (const match of matches) {
// If matches overlap there must be something wrong with the matcher
if (lastIndex > match.start)
throw new BentleyError(
BentleyStatus.ERROR,
"renderText: matcher returned overlapping matches"
);
if (lastIndex < match.start)
parts.push(
renderTextPart(text.substring(lastIndex, match.start), highlight)
);
const anchorText = text.substring(match.start, match.end);
parts.push(renderTag(anchorText, links, highlight));
lastIndex = match.end;
}
if (text.length > lastIndex)
parts.push(renderTextPart(text.substring(lastIndex), highlight));
// Need to map, because React complains about the lack of keys
return parts.map((part, index) => (
<React.Fragment key={index}>{part}</React.Fragment>
));
}
function renderHighlighted(
text: string,
highlight: (text: string) => React.ReactNode
) {
return highlight(text);
}
/** Renders anchor tag by wrapping or splitting provided text
* @public
*/
export const renderLinks = (
text: string,
links: LinkElementsInfo,
highlight?: (text: string) => React.ReactNode
): React.ReactNode => {
return renderText(text, links, highlight);
};
/** If record has links, wraps stringValue in them, otherwise returns unchanged stringValue
* Optionally it can highlight text
* @public
*/
export const withLinks = (
stringValue: string,
links?: LinkElementsInfo,
highlight?: (text: string) => React.ReactNode
): React.ReactNode => {
if (links) return renderLinks(stringValue, links, highlight);
if (highlight) return renderHighlighted(stringValue, highlight);
return stringValue;
};
/**
* Properties for [[LinksRenderer]] component.
* @public
*/
export interface LinksRendererProps {
value: string;
links?: LinkElementsInfo;
highlighter?: (text: string) => React.ReactNode;
}
/**
* React component for rendering string with links.
* @public
*/
export function LinksRenderer(props: LinksRendererProps) {
return <>{withLinks(props.value, props.links, props.highlighter)}</>;
}