-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy pathTokenAnnotator.tsx
More file actions
102 lines (83 loc) · 2.74 KB
/
TokenAnnotator.tsx
File metadata and controls
102 lines (83 loc) · 2.74 KB
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
import React from 'react'
import Mark, {MarkProps} from './Mark'
import {selectionIsEmpty, selectionIsBackwards, splitTokensWithOffsets} from './utils'
import {Span} from './span'
interface TokenProps {
i: number
content: string
}
interface TokenSpan {
start: number
end: number
tokens: string[]
}
const Token: React.SFC<TokenProps> = props => {
return <span data-i={props.i}>{props.content} </span>
}
export interface TokenAnnotatorProps<T>
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
tokens: string[]
value: T[]
onChange: (value: T[]) => any
getSpan?: (span: TokenSpan) => T
renderMark?: (props: MarkProps) => JSX.Element
isIntersectToHighlightedText?: boolean
}
const TokenAnnotator = <T extends Span>(props: TokenAnnotatorProps<T>) => {
const renderMark = props.renderMark || (props => <Mark {...props} />)
const getSpan = (span: TokenSpan): T => {
if (props.getSpan) return props.getSpan(span)
return {start: span.start, end: span.end} as T
}
const handleMouseUp = () => {
if (!props.onChange) return
const selection = window.getSelection()
if (selectionIsEmpty(selection)) return
if (
!selection.anchorNode.parentElement.hasAttribute('data-i') ||
!selection.focusNode.parentElement.hasAttribute('data-i')
) {
window.getSelection().empty()
return false
}
let start = parseInt(selection.anchorNode.parentElement.getAttribute('data-i'), 10)
let end = parseInt(selection.focusNode.parentElement.getAttribute('data-i'), 10)
if (selectionIsBackwards(selection)) {
;[start, end] = [end, start]
}
if (props.isIntersectToHighlightedText) {
const splitIndex = props.value.findIndex(s => s.start >= start && s.end <= end)
if (splitIndex >= 0) {
return
}
}
end += 1
props.onChange([...props.value, getSpan({start, end, tokens: props.tokens.slice(start, end)})])
window.getSelection().empty()
}
const handleSplitClick = ({start, end}) => {
// Find and remove the matching split.
const splitIndex = props.value.findIndex(s => s.start === start && s.end === end)
if (splitIndex >= 0) {
props.onChange([...props.value.slice(0, splitIndex), ...props.value.slice(splitIndex + 1)])
}
}
const {tokens, value, onChange, getSpan: _, ...divProps} = props
const splits = splitTokensWithOffsets(tokens, value)
return (
<div {...divProps} onMouseUp={handleMouseUp}>
{splits.map((split, i) =>
split.mark ? (
renderMark({
key: `${split.start}-${split.end}`,
...split,
onClick: handleSplitClick,
})
) : (
<Token key={split.i} {...split} />
)
)}
</div>
)
}
export default TokenAnnotator