@pie-lib/text-select 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/legend.d.ts +13 -0
  5. package/dist/legend.d.ts.map +1 -0
  6. package/dist/legend.js +64 -0
  7. package/dist/text-select.d.ts +35 -0
  8. package/dist/text-select.d.ts.map +1 -0
  9. package/dist/text-select.js +53 -0
  10. package/dist/token-select/index.d.ts +39 -0
  11. package/dist/token-select/index.d.ts.map +1 -0
  12. package/dist/token-select/index.js +102 -0
  13. package/dist/token-select/token.d.ts +33 -0
  14. package/dist/token-select/token.d.ts.map +1 -0
  15. package/dist/token-select/token.js +134 -0
  16. package/dist/tokenizer/builder.d.ts +28 -0
  17. package/dist/tokenizer/builder.d.ts.map +1 -0
  18. package/dist/tokenizer/builder.js +124 -0
  19. package/dist/tokenizer/controls.d.ts +24 -0
  20. package/dist/tokenizer/controls.d.ts.map +1 -0
  21. package/dist/tokenizer/controls.js +68 -0
  22. package/dist/tokenizer/index.d.ts +36 -0
  23. package/dist/tokenizer/index.d.ts.map +1 -0
  24. package/dist/tokenizer/index.js +91 -0
  25. package/dist/tokenizer/selection-utils.d.ts +11 -0
  26. package/dist/tokenizer/selection-utils.d.ts.map +1 -0
  27. package/dist/tokenizer/selection-utils.js +18 -0
  28. package/dist/tokenizer/token-text.d.ts +28 -0
  29. package/dist/tokenizer/token-text.d.ts.map +1 -0
  30. package/dist/tokenizer/token-text.js +85 -0
  31. package/dist/utils.d.ts +13 -0
  32. package/dist/utils.d.ts.map +1 -0
  33. package/dist/utils.js +21 -0
  34. package/package.json +39 -0
  35. package/src/index.ts +18 -0
  36. package/src/legend.tsx +112 -0
  37. package/src/text-select.tsx +89 -0
  38. package/src/token-select/index.tsx +181 -0
  39. package/src/token-select/token.tsx +233 -0
  40. package/src/tokenizer/builder.ts +268 -0
  41. package/src/tokenizer/controls.tsx +81 -0
  42. package/src/tokenizer/index.tsx +154 -0
  43. package/src/tokenizer/selection-utils.ts +59 -0
  44. package/src/tokenizer/token-text.tsx +145 -0
  45. package/src/utils.tsx +66 -0
@@ -0,0 +1,268 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/tokenizer/builder.js
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import { clone, compact } from 'lodash-es';
12
+ import English from '@pie-framework/parse-english';
13
+
14
+ const g = (str, node) => {
15
+ if (node.children) {
16
+ return node.children.reduce(g, str);
17
+ } else if (node.value) {
18
+ return str + node.value;
19
+ } else {
20
+ return str;
21
+ }
22
+ };
23
+
24
+ const getParagraph = (p) => g('', p);
25
+
26
+ const getSentence = (s) => g('', s);
27
+
28
+ const getWord = (w) => g('', w);
29
+
30
+ export const paragraphs = (text) => {
31
+ const tree = new English().parse(text);
32
+
33
+ const out = tree.children.reduce((acc, child) => {
34
+ if (child.type === 'ParagraphNode') {
35
+ const paragraph = {
36
+ text: getParagraph(child),
37
+ start: child.position.start.offset,
38
+ end: child.position.end.offset,
39
+ };
40
+
41
+ return acc.concat([paragraph]);
42
+ } else {
43
+ return acc;
44
+ }
45
+ }, []);
46
+
47
+ return out;
48
+ };
49
+
50
+ export const handleSentence = (child, acc) => {
51
+ const sentenceChilds = [];
52
+ // we parse the children of the sentence
53
+ let newAcc = child.children.reduce(function (acc, child) {
54
+ // if we find a whitespace node that's \n, we end the sentence
55
+ if (child.type === 'WhiteSpaceNode' && child.value === '\n') {
56
+ if (sentenceChilds.length) {
57
+ const firstWord = sentenceChilds[0];
58
+ // we create a sentence starting from the first word until the new line
59
+ const sentence = {
60
+ text: sentenceChilds.map((d) => getSentence(d)).join(''),
61
+ start: firstWord.position.start.offset,
62
+ end: child.position.start.offset,
63
+ };
64
+
65
+ // we remove all the elements from the array
66
+ sentenceChilds.splice(0, sentenceChilds.length);
67
+ return acc.concat([sentence]);
68
+ }
69
+ } else {
70
+ // otherwise we add it to the array that contains the child forming a sentence
71
+ sentenceChilds.push(child);
72
+ }
73
+
74
+ return acc;
75
+ }, acc);
76
+
77
+ // we treat the case when no \n character is found at the end
78
+ // so we create a sentence from the last words or white spaces found
79
+ if (sentenceChilds.length) {
80
+ const firstWord = sentenceChilds[0];
81
+ const lastWord = sentenceChilds[sentenceChilds.length - 1];
82
+ const sentence = {
83
+ text: sentenceChilds.map((d) => getSentence(d)).join(''),
84
+ start: firstWord.position.start.offset,
85
+ end: lastWord.position.end.offset,
86
+ };
87
+
88
+ newAcc = newAcc.concat([sentence]);
89
+
90
+ sentenceChilds.splice(0, sentenceChilds.length);
91
+ }
92
+
93
+ return newAcc;
94
+ };
95
+
96
+ export const sentences = (text) => {
97
+ const tree = new English().parse(text);
98
+
99
+ const out = tree.children.reduce((acc, child) => {
100
+ if (child.type === 'ParagraphNode') {
101
+ return child.children.reduce((acc, child) => {
102
+ if (child.type === 'SentenceNode') {
103
+ const newAcc = handleSentence(child, acc);
104
+
105
+ return newAcc || acc;
106
+ } else {
107
+ return acc;
108
+ }
109
+ }, acc);
110
+ } else {
111
+ return acc;
112
+ }
113
+ }, []);
114
+
115
+ return out;
116
+ };
117
+ export const words = (text) => {
118
+ const tree = new English().parse(text);
119
+
120
+ const out = tree.children.reduce((acc, child) => {
121
+ if (child.type === 'ParagraphNode') {
122
+ return child.children.reduce((acc, child) => {
123
+ if (child.type === 'SentenceNode') {
124
+ return child.children.reduce((acc, child) => {
125
+ if (child.type === 'WordNode') {
126
+ const node = {
127
+ text: getWord(child),
128
+ start: child.position.start.offset,
129
+ end: child.position.end.offset,
130
+ };
131
+ return acc.concat([node]);
132
+ } else {
133
+ return acc;
134
+ }
135
+ }, acc);
136
+ } else {
137
+ return acc;
138
+ }
139
+ }, acc);
140
+ } else {
141
+ return acc;
142
+ }
143
+ }, []);
144
+
145
+ return out;
146
+ };
147
+
148
+ class Intersection {
149
+ constructor(results) {
150
+ this.results = results;
151
+ }
152
+
153
+ get hasOverlap() {
154
+ return this.results.filter((r) => r.type === 'overlap').length > 0;
155
+ }
156
+
157
+ get surroundedTokens() {
158
+ return this.results.filter((r) => r.type === 'within-selection').map((t) => t.token);
159
+ }
160
+ }
161
+ /**
162
+ * get intersection info for the selection in relation to tokens.
163
+ * @param {{start: number, end: number}} selection
164
+ * @param {{start: number, end: number}[]} tokens
165
+ * @return {tokens: [], type: 'overlap|no-overlap|contains'}
166
+ */
167
+ export const intersection = (selection, tokens) => {
168
+ const { start, end } = selection;
169
+
170
+ const startsWithin = (t) => start >= t.start && start < t.end;
171
+ const endsWithin = (t) => end > t.start && end <= t.end;
172
+
173
+ const mapped = tokens.map((t) => {
174
+ if (start === t.start && end === t.end) {
175
+ return { token: t, type: 'exact-fit' };
176
+ } else if (start <= t.start && end >= t.end) {
177
+ return { token: t, type: 'within-selection' };
178
+ } else if (startsWithin(t) || endsWithin(t)) {
179
+ return { token: t, type: 'overlap' };
180
+ }
181
+ });
182
+ return new Intersection(compact(mapped));
183
+ };
184
+
185
+ export const sort = (tokens) => {
186
+ if (!Array.isArray(tokens)) {
187
+ return tokens;
188
+ } else {
189
+ const out = clone(tokens);
190
+ out.sort((a, b) => {
191
+ const s = a.start < b.start ? -1 : a.start > b.start ? 1 : 0;
192
+ const e = a.end < b.end ? -1 : a.end > b.end ? 1 : 0;
193
+ if (s === -1 && e !== -1) {
194
+ throw new Error(`sort does not support intersecting tokens. a: ${a.start}-${a.end}, b: ${b.start}-${b.end}`);
195
+ }
196
+ return s;
197
+ });
198
+ return out;
199
+ }
200
+ };
201
+
202
+ export const normalize = (textToNormalize, tokens) => {
203
+ // making sure text provided is a string
204
+ const text = textToNormalize || '';
205
+
206
+ if (!Array.isArray(tokens) || tokens.length === 0) {
207
+ return [
208
+ {
209
+ text,
210
+ start: 0,
211
+ end: text.length,
212
+ },
213
+ ];
214
+ }
215
+
216
+ const out = sort(tokens).reduce(
217
+ (acc, t, index, outer) => {
218
+ let tokens = [];
219
+ const lastIndex = acc.lastIndex;
220
+
221
+ if (t.start === lastIndex) {
222
+ tokens = [
223
+ {
224
+ text: text.substring(lastIndex, t.end),
225
+ start: lastIndex,
226
+ end: t.end,
227
+ predefined: true,
228
+ correct: t.correct,
229
+ isMissing: t.isMissing,
230
+ },
231
+ ];
232
+ } else if (lastIndex < t.start) {
233
+ tokens = [
234
+ {
235
+ text: text.substring(lastIndex, t.start),
236
+ start: lastIndex,
237
+ end: t.start,
238
+ },
239
+ {
240
+ text: text.substring(t.start, t.end),
241
+ start: t.start,
242
+ end: t.end,
243
+ predefined: true,
244
+ correct: t.correct,
245
+ isMissing: t.isMissing,
246
+ },
247
+ ];
248
+ }
249
+
250
+ if (index === outer.length - 1 && t.end < text.length) {
251
+ const last = {
252
+ text: text.substring(t.end),
253
+ start: t.end,
254
+ end: text.length,
255
+ };
256
+ tokens.push(last);
257
+ }
258
+
259
+ return {
260
+ lastIndex: tokens.length ? tokens[tokens.length - 1].end : lastIndex,
261
+ result: acc.result.concat(tokens),
262
+ };
263
+ },
264
+ { result: [], lastIndex: 0 },
265
+ );
266
+
267
+ return out.result;
268
+ };
@@ -0,0 +1,81 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/tokenizer/controls.jsx
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import React from 'react';
12
+ import PropTypes from 'prop-types';
13
+ import Button from '@mui/material/Button';
14
+ import { styled } from '@mui/material/styles';
15
+ import Switch from '@mui/material/Switch';
16
+ import FormControlLabel from '@mui/material/FormControlLabel';
17
+ import { color } from '@pie-lib/render-ui';
18
+
19
+ const StyledControls: any = styled('div')(() => ({
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ justifyContent: 'space-between',
23
+ }));
24
+
25
+ const StyledButton: any = styled(Button)(({ theme }) => ({
26
+ marginRight: theme.spacing(1),
27
+ }));
28
+
29
+ const StyledSwitch: any = styled(Switch)(() => ({
30
+ '& .MuiSwitch-thumb': {
31
+ '&.Mui-checked': {
32
+ color: `${color.tertiary()} !important`,
33
+ },
34
+ },
35
+ '& .MuiSwitch-track': {
36
+ '&.Mui-checked': {
37
+ backgroundColor: `${color.tertiaryLight()} !important`,
38
+ },
39
+ },
40
+ }));
41
+
42
+ export class Controls extends React.Component {
43
+ static propTypes = {
44
+ onClear: PropTypes.func.isRequired,
45
+ onWords: PropTypes.func.isRequired,
46
+ onSentences: PropTypes.func.isRequired,
47
+ onParagraphs: PropTypes.func.isRequired,
48
+ setCorrectMode: PropTypes.bool.isRequired,
49
+ onToggleCorrectMode: PropTypes.func.isRequired,
50
+ };
51
+
52
+ static defaultProps = {};
53
+
54
+ render() {
55
+ const { onClear, onWords, onSentences, onParagraphs, setCorrectMode, onToggleCorrectMode } = this.props;
56
+
57
+ return (
58
+ <StyledControls>
59
+ <div>
60
+ <StyledButton onClick={onWords} size="small" color="primary" disabled={setCorrectMode}>
61
+ Words
62
+ </StyledButton>
63
+ <StyledButton onClick={onSentences} size="small" color="primary" disabled={setCorrectMode}>
64
+ Sentences
65
+ </StyledButton>
66
+ <StyledButton onClick={onParagraphs} size="small" color="primary" disabled={setCorrectMode}>
67
+ Paragraphs
68
+ </StyledButton>
69
+ <StyledButton size="small" color="secondary" onClick={onClear} disabled={setCorrectMode}>
70
+ Clear
71
+ </StyledButton>
72
+ </div>
73
+ <FormControlLabel
74
+ control={<StyledSwitch checked={setCorrectMode} onChange={onToggleCorrectMode} />}
75
+ label="Set correct answers"
76
+ />
77
+ </StyledControls>
78
+ );
79
+ }
80
+ }
81
+ export default Controls;
@@ -0,0 +1,154 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/tokenizer/index.jsx
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import React from 'react';
12
+ import PropTypes from 'prop-types';
13
+ import Controls from './controls.js';
14
+ import { styled } from '@mui/material/styles';
15
+ import { paragraphs, sentences, words } from './builder.js';
16
+ import { clone, differenceWith, isEqual } from 'lodash-es';
17
+ import { noSelect } from '@pie-lib/style-utils';
18
+ import TokenText from './token-text.js';
19
+
20
+ const StyledText: any = styled('div')(({ disabled }) => ({
21
+ whiteSpace: 'pre-wrap',
22
+
23
+ ...(disabled && {
24
+ ...noSelect(),
25
+ }),
26
+ }));
27
+
28
+ export class Tokenizer extends React.Component {
29
+ static propTypes = {
30
+ text: PropTypes.string.isRequired,
31
+ tokens: PropTypes.arrayOf(
32
+ PropTypes.shape({
33
+ text: PropTypes.string,
34
+ correct: PropTypes.bool,
35
+ start: PropTypes.number,
36
+ end: PropTypes.number,
37
+ }),
38
+ ),
39
+ onChange: PropTypes.func.isRequired,
40
+ };
41
+
42
+ static defaultProps = {};
43
+
44
+ constructor(props) {
45
+ super(props);
46
+ this.state = {
47
+ setCorrectMode: false,
48
+ mode: '',
49
+ };
50
+ }
51
+
52
+ onChangeHandler: any = (token, mode) => {
53
+ this.props.onChange(token, mode);
54
+
55
+ this.setState({
56
+ mode,
57
+ });
58
+ };
59
+
60
+ toggleCorrectMode = () => this.setState({ setCorrectMode: !this.state.setCorrectMode });
61
+
62
+ clear: any = () => {
63
+ this.onChangeHandler([], '');
64
+ };
65
+
66
+ buildTokens: any = (type, fn) => {
67
+ const { text } = this.props;
68
+ const tokens = fn(text);
69
+
70
+ this.onChangeHandler(tokens, type);
71
+ };
72
+
73
+ selectToken: any = (newToken, tokensToRemove) => {
74
+ const { tokens } = this.props;
75
+ const update = differenceWith(clone(tokens), tokensToRemove, isEqual);
76
+
77
+ update.push(newToken);
78
+ this.onChangeHandler(update, this.state.mode);
79
+ };
80
+
81
+ tokenClick: any = (token) => {
82
+ const { setCorrectMode } = this.state;
83
+
84
+ if (setCorrectMode) {
85
+ this.setCorrect(token);
86
+ } else {
87
+ this.removeToken(token);
88
+ }
89
+ };
90
+
91
+ tokenIndex: any = (token) => {
92
+ const { tokens } = this.props;
93
+
94
+ return tokens.findIndex((t) => {
95
+ return t.text == token.text && t.start == token.start && t.end == token.end;
96
+ });
97
+ };
98
+
99
+ setCorrect: any = (token) => {
100
+ const { tokens } = this.props;
101
+ const index = this.tokenIndex(token);
102
+ if (index !== -1) {
103
+ const t = tokens[index];
104
+
105
+ t.correct = !t.correct;
106
+
107
+ const update = clone(tokens);
108
+
109
+ update.splice(index, 1, t);
110
+ this.onChangeHandler(update, this.state.mode);
111
+ }
112
+ };
113
+
114
+ removeToken: any = (token) => {
115
+ const { tokens } = this.props;
116
+
117
+ const index = this.tokenIndex(token);
118
+ if (index !== -1) {
119
+ const update = clone(tokens);
120
+
121
+ update.splice(index, 1);
122
+
123
+ this.onChangeHandler(update, this.state.mode);
124
+ }
125
+ };
126
+
127
+ render() {
128
+ const { text, tokens } = this.props;
129
+ const { setCorrectMode } = this.state;
130
+
131
+ return (
132
+ <div>
133
+ <Controls
134
+ onClear={this.clear}
135
+ onWords={() => this.buildTokens('words', words)}
136
+ onSentences={() => this.buildTokens('sentence', sentences)}
137
+ onParagraphs={() => this.buildTokens('paragraphs', paragraphs)}
138
+ setCorrectMode={setCorrectMode}
139
+ onToggleCorrectMode={this.toggleCorrectMode}
140
+ />
141
+ <StyledText
142
+ disabled={setCorrectMode}
143
+ as={TokenText}
144
+ text={text}
145
+ tokens={tokens}
146
+ onTokenClick={this.tokenClick}
147
+ onSelectToken={this.selectToken}
148
+ />
149
+ </div>
150
+ );
151
+ }
152
+ }
153
+
154
+ export default Tokenizer;
@@ -0,0 +1,59 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/tokenizer/selection-utils.js
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ export const clearSelection = () => {
12
+ if (document.getSelection) {
13
+ // for all new browsers (IE9+, Chrome, Firefox)
14
+ document.getSelection().removeAllRanges();
15
+ document.getSelection().addRange(document.createRange());
16
+ } else if (window.getSelection) {
17
+ // equals with the document.getSelection (MSDN info)
18
+ if (window.getSelection().removeAllRanges) {
19
+ // for all new browsers (IE9+, Chrome, Firefox)
20
+ window.getSelection().removeAllRanges();
21
+ window.getSelection().addRange(document.createRange());
22
+ } else if (window.getSelection().empty) {
23
+ // Chrome supports this as well
24
+ window.getSelection().empty();
25
+ }
26
+ } else if (document.selection) {
27
+ // IE8-
28
+ document.selection.empty();
29
+ }
30
+ };
31
+
32
+ export const getCaretCharacterOffsetWithin = (element) => {
33
+ var caretOffset = 0;
34
+ var doc = element.ownerDocument || element.document;
35
+ var win = doc.defaultView || doc.parentWindow;
36
+ var sel;
37
+ if (typeof win.getSelection !== 'undefined') {
38
+ sel = win.getSelection();
39
+ if (sel.rangeCount > 0) {
40
+ var range = win.getSelection().getRangeAt(0);
41
+ var selected = range.toString().length;
42
+ var preCaretRange = range.cloneRange();
43
+ preCaretRange.selectNodeContents(element);
44
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
45
+ if (selected) {
46
+ caretOffset = preCaretRange.toString().length - selected;
47
+ } else {
48
+ caretOffset = preCaretRange.toString().length;
49
+ }
50
+ }
51
+ } else if ((sel = doc.selection) && sel.type !== 'Control') {
52
+ var textRange = sel.createRange();
53
+ var preCaretTextRange = doc.body.createTextRange();
54
+ preCaretTextRange.moveToElementText(element);
55
+ preCaretTextRange.setEndPoint('EndToEnd', textRange);
56
+ caretOffset = preCaretTextRange.text.length;
57
+ }
58
+ return caretOffset;
59
+ };
@@ -0,0 +1,145 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/tokenizer/token-text.jsx
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import React from 'react';
12
+ import PropTypes from 'prop-types';
13
+ import { styled } from '@mui/material/styles';
14
+ import { intersection, normalize } from './builder.js';
15
+ import debug from 'debug';
16
+ import classNames from 'classnames';
17
+
18
+ import { clearSelection, getCaretCharacterOffsetWithin } from './selection-utils.js';
19
+
20
+ import { green, yellow } from '@mui/material/colors';
21
+
22
+ const log = debug('@pie-lib:text-select:token-text');
23
+
24
+ const StyledText: any = styled('span')(() => ({
25
+ '&.predefined': {
26
+ cursor: 'pointer',
27
+ backgroundColor: yellow[100],
28
+ border: `dashed 0px ${yellow[700]}`,
29
+ // we need this for nested tokenized elements like paragraphs, where p is inside span
30
+ '& *': {
31
+ cursor: 'pointer',
32
+ backgroundColor: yellow[100],
33
+ border: `dashed 0px ${yellow[700]}`,
34
+ },
35
+ },
36
+ '&.correct': {
37
+ backgroundColor: green[500],
38
+ '& *': {
39
+ backgroundColor: green[500],
40
+ },
41
+ },
42
+ }));
43
+
44
+ export const Text = ({ text, predefined, onClick, correct }) => {
45
+ const formattedText = (text || '').replace(/\n/g, '<br>');
46
+
47
+ if (predefined) {
48
+ const className = classNames('predefined', correct && 'correct');
49
+
50
+ return <StyledText onClick={onClick} className={className} dangerouslySetInnerHTML={{ __html: formattedText }} />;
51
+ } else {
52
+ return <span dangerouslySetInnerHTML={{ __html: formattedText }} />;
53
+ }
54
+ };
55
+
56
+ const notAllowedCharacters = ['\n', ' ', '\t'];
57
+
58
+ export default class TokenText extends React.Component {
59
+ static propTypes = {
60
+ text: PropTypes.string.isRequired,
61
+ tokens: PropTypes.array.isRequired,
62
+ onTokenClick: PropTypes.func.isRequired,
63
+ onSelectToken: PropTypes.func.isRequired,
64
+ className: PropTypes.string,
65
+ };
66
+
67
+ /*
68
+ Change this to onClick instead of mouseUp because previously, in some cases
69
+ the onClick event from the <Text /> component was called right after the user
70
+ selected token and that token was then removed because the setCorrectMode was not true.
71
+
72
+ const { setCorrectMode } = this.state;
73
+
74
+ if (setCorrectMode) {
75
+ this.setCorrect(token);
76
+ } else {
77
+ this.removeToken(token);
78
+ }
79
+ */
80
+ onClick: any = (event) => {
81
+ const { onSelectToken, text, tokens } = this.props;
82
+
83
+ event.preventDefault();
84
+
85
+ if (typeof window === 'undefined') {
86
+ return;
87
+ }
88
+
89
+ const selection = window.getSelection();
90
+ const textSelected = selection.toString();
91
+
92
+ if (textSelected.length > 0 && notAllowedCharacters.indexOf(textSelected) < 0) {
93
+ if (this.root) {
94
+ let offset = getCaretCharacterOffsetWithin(this.root);
95
+ /*
96
+ Since we implemented new line functionality (\n) using <br /> dom elements
97
+ and window.getSelection is not taking that into consideration, the offset might
98
+ be off by a few characters.
99
+
100
+ To combat that, we check if the selected text is right at the beginning of the offset.
101
+
102
+ If it's not, we add the additional offset in order for that to be accurate
103
+ */
104
+ const newLineOffset = text.slice(offset).indexOf(textSelected);
105
+
106
+ offset += newLineOffset;
107
+
108
+ if (offset !== undefined) {
109
+ const endIndex = offset + textSelected.length;
110
+
111
+ if (endIndex <= text.length) {
112
+ const i = intersection({ start: offset, end: endIndex }, tokens);
113
+ if (i.hasOverlap) {
114
+ log('hasOverlap - do nothing');
115
+ clearSelection();
116
+ } else {
117
+ const tokensToRemove = i.surroundedTokens;
118
+ const token = {
119
+ text: textSelected,
120
+ start: offset,
121
+ end: endIndex,
122
+ };
123
+
124
+ onSelectToken(token, tokensToRemove);
125
+ clearSelection();
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ };
132
+
133
+ render() {
134
+ const { text, tokens, className, onTokenClick } = this.props;
135
+ const normalized = normalize(text, tokens);
136
+
137
+ return (
138
+ <div className={className} ref={(r) => (this.root = r)} onClick={this.onClick}>
139
+ {normalized.map((t, index) => {
140
+ return <Text key={index} {...t} onClick={() => onTokenClick(t)} />;
141
+ })}
142
+ </div>
143
+ );
144
+ }
145
+ }