@pie-lib/text-select 1.28.1-next.0 → 1.29.1
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.
- package/esm/index.js +1307 -0
- package/esm/index.js.map +1 -0
- package/lib/token-select/index.js +10 -2
- package/lib/token-select/index.js.map +1 -1
- package/package.json +14 -7
- package/src/token-select/index.jsx +3 -0
package/esm/index.js
ADDED
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Button from '@material-ui/core/Button';
|
|
4
|
+
import { withStyles } from '@material-ui/core/styles';
|
|
5
|
+
import Switch from '@material-ui/core/Switch';
|
|
6
|
+
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
|
7
|
+
import { color } from '@pie-lib/render-ui';
|
|
8
|
+
import classNames from 'classnames';
|
|
9
|
+
import compact from 'lodash/compact';
|
|
10
|
+
import English from '@pie-framework/parse-english';
|
|
11
|
+
import clone from 'lodash/clone';
|
|
12
|
+
import isEqual from 'lodash/isEqual';
|
|
13
|
+
import differenceWith from 'lodash/differenceWith';
|
|
14
|
+
import { noSelect } from '@pie-lib/style-utils';
|
|
15
|
+
import yellow from '@material-ui/core/colors/yellow';
|
|
16
|
+
import green from '@material-ui/core/colors/green';
|
|
17
|
+
import debug from 'debug';
|
|
18
|
+
import Check from '@material-ui/icons/Check';
|
|
19
|
+
import Close from '@material-ui/icons/Close';
|
|
20
|
+
import { renderToString } from 'react-dom/server';
|
|
21
|
+
import Translator from '@pie-lib/translator';
|
|
22
|
+
|
|
23
|
+
function _extends() {
|
|
24
|
+
_extends = Object.assign || function (target) {
|
|
25
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
26
|
+
var source = arguments[i];
|
|
27
|
+
|
|
28
|
+
for (var key in source) {
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
30
|
+
target[key] = source[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return target;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return _extends.apply(this, arguments);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Controls extends React.Component {
|
|
42
|
+
render() {
|
|
43
|
+
const {
|
|
44
|
+
classes,
|
|
45
|
+
onClear,
|
|
46
|
+
onWords,
|
|
47
|
+
onSentences,
|
|
48
|
+
onParagraphs,
|
|
49
|
+
setCorrectMode,
|
|
50
|
+
onToggleCorrectMode
|
|
51
|
+
} = this.props;
|
|
52
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
53
|
+
className: classes.controls
|
|
54
|
+
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Button, {
|
|
55
|
+
onClick: onWords,
|
|
56
|
+
className: classes.button,
|
|
57
|
+
size: "small",
|
|
58
|
+
color: "primary",
|
|
59
|
+
disabled: setCorrectMode
|
|
60
|
+
}, "Words"), /*#__PURE__*/React.createElement(Button, {
|
|
61
|
+
onClick: onSentences,
|
|
62
|
+
className: classes.button,
|
|
63
|
+
size: "small",
|
|
64
|
+
color: "primary",
|
|
65
|
+
disabled: setCorrectMode
|
|
66
|
+
}, "Sentences"), /*#__PURE__*/React.createElement(Button, {
|
|
67
|
+
onClick: onParagraphs,
|
|
68
|
+
className: classes.button,
|
|
69
|
+
size: "small",
|
|
70
|
+
color: "primary",
|
|
71
|
+
disabled: setCorrectMode
|
|
72
|
+
}, "Paragraphs"), /*#__PURE__*/React.createElement(Button, {
|
|
73
|
+
className: classes.button,
|
|
74
|
+
size: "small",
|
|
75
|
+
color: "secondary",
|
|
76
|
+
onClick: onClear,
|
|
77
|
+
disabled: setCorrectMode
|
|
78
|
+
}, "Clear")), /*#__PURE__*/React.createElement(FormControlLabel, {
|
|
79
|
+
control: /*#__PURE__*/React.createElement(Switch, {
|
|
80
|
+
classes: {
|
|
81
|
+
checked: classes.checkedThumb,
|
|
82
|
+
bar: classNames({
|
|
83
|
+
[classes.checkedBar]: setCorrectMode
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
checked: setCorrectMode,
|
|
87
|
+
onChange: onToggleCorrectMode
|
|
88
|
+
}),
|
|
89
|
+
label: "Set correct answers"
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
Controls.propTypes = {
|
|
95
|
+
classes: PropTypes.object.isRequired,
|
|
96
|
+
onClear: PropTypes.func.isRequired,
|
|
97
|
+
onWords: PropTypes.func.isRequired,
|
|
98
|
+
onSentences: PropTypes.func.isRequired,
|
|
99
|
+
onParagraphs: PropTypes.func.isRequired,
|
|
100
|
+
setCorrectMode: PropTypes.bool.isRequired,
|
|
101
|
+
onToggleCorrectMode: PropTypes.func.isRequired
|
|
102
|
+
};
|
|
103
|
+
Controls.defaultProps = {};
|
|
104
|
+
var Controls$1 = withStyles(theme => ({
|
|
105
|
+
button: {
|
|
106
|
+
marginRight: theme.spacing.unit
|
|
107
|
+
},
|
|
108
|
+
controls: {
|
|
109
|
+
display: 'flex',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
justifyContent: 'space-between'
|
|
112
|
+
},
|
|
113
|
+
checkedThumb: {
|
|
114
|
+
color: `${color.tertiary()} !important`
|
|
115
|
+
},
|
|
116
|
+
checkedBar: {
|
|
117
|
+
backgroundColor: `${color.tertiaryLight()} !important`
|
|
118
|
+
}
|
|
119
|
+
}))(Controls);
|
|
120
|
+
|
|
121
|
+
const g = (str, node) => {
|
|
122
|
+
if (node.children) {
|
|
123
|
+
return node.children.reduce(g, str);
|
|
124
|
+
} else if (node.value) {
|
|
125
|
+
return str + node.value;
|
|
126
|
+
} else {
|
|
127
|
+
return str;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const getParagraph = p => g('', p);
|
|
132
|
+
|
|
133
|
+
const getSentence = s => g('', s);
|
|
134
|
+
|
|
135
|
+
const getWord = w => g('', w);
|
|
136
|
+
|
|
137
|
+
const paragraphs = text => {
|
|
138
|
+
const tree = new English().parse(text);
|
|
139
|
+
const out = tree.children.reduce((acc, child) => {
|
|
140
|
+
if (child.type === 'ParagraphNode') {
|
|
141
|
+
const paragraph = {
|
|
142
|
+
text: getParagraph(child),
|
|
143
|
+
start: child.position.start.offset,
|
|
144
|
+
end: child.position.end.offset
|
|
145
|
+
};
|
|
146
|
+
return acc.concat([paragraph]);
|
|
147
|
+
} else {
|
|
148
|
+
return acc;
|
|
149
|
+
}
|
|
150
|
+
}, []);
|
|
151
|
+
return out;
|
|
152
|
+
};
|
|
153
|
+
const handleSentence = (child, acc) => {
|
|
154
|
+
const sentenceChilds = []; // we parse the children of the sentence
|
|
155
|
+
|
|
156
|
+
let newAcc = child.children.reduce(function (acc, child) {
|
|
157
|
+
// if we find a whitespace node that's \n, we end the sentence
|
|
158
|
+
if (child.type === 'WhiteSpaceNode' && child.value === '\n') {
|
|
159
|
+
if (sentenceChilds.length) {
|
|
160
|
+
const firstWord = sentenceChilds[0]; // we create a sentence starting from the first word until the new line
|
|
161
|
+
|
|
162
|
+
const sentence = {
|
|
163
|
+
text: sentenceChilds.map(d => getSentence(d)).join(''),
|
|
164
|
+
start: firstWord.position.start.offset,
|
|
165
|
+
end: child.position.start.offset
|
|
166
|
+
}; // we remove all the elements from the array
|
|
167
|
+
|
|
168
|
+
sentenceChilds.splice(0, sentenceChilds.length);
|
|
169
|
+
return acc.concat([sentence]);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// otherwise we add it to the array that contains the child forming a sentence
|
|
173
|
+
sentenceChilds.push(child);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return acc;
|
|
177
|
+
}, acc); // we treat the case when no \n character is found at the end
|
|
178
|
+
// so we create a sentence from the last words or white spaces found
|
|
179
|
+
|
|
180
|
+
if (sentenceChilds.length) {
|
|
181
|
+
const firstWord = sentenceChilds[0];
|
|
182
|
+
const lastWord = sentenceChilds[sentenceChilds.length - 1];
|
|
183
|
+
const sentence = {
|
|
184
|
+
text: sentenceChilds.map(d => getSentence(d)).join(''),
|
|
185
|
+
start: firstWord.position.start.offset,
|
|
186
|
+
end: lastWord.position.end.offset
|
|
187
|
+
};
|
|
188
|
+
newAcc = newAcc.concat([sentence]);
|
|
189
|
+
sentenceChilds.splice(0, sentenceChilds.length);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return newAcc;
|
|
193
|
+
};
|
|
194
|
+
const sentences = text => {
|
|
195
|
+
const tree = new English().parse(text);
|
|
196
|
+
const out = tree.children.reduce((acc, child) => {
|
|
197
|
+
if (child.type === 'ParagraphNode') {
|
|
198
|
+
return child.children.reduce((acc, child) => {
|
|
199
|
+
if (child.type === 'SentenceNode') {
|
|
200
|
+
const newAcc = handleSentence(child, acc);
|
|
201
|
+
return newAcc || acc;
|
|
202
|
+
} else {
|
|
203
|
+
return acc;
|
|
204
|
+
}
|
|
205
|
+
}, acc);
|
|
206
|
+
} else {
|
|
207
|
+
return acc;
|
|
208
|
+
}
|
|
209
|
+
}, []);
|
|
210
|
+
return out;
|
|
211
|
+
};
|
|
212
|
+
const words = text => {
|
|
213
|
+
const tree = new English().parse(text);
|
|
214
|
+
const out = tree.children.reduce((acc, child) => {
|
|
215
|
+
if (child.type === 'ParagraphNode') {
|
|
216
|
+
return child.children.reduce((acc, child) => {
|
|
217
|
+
if (child.type === 'SentenceNode') {
|
|
218
|
+
return child.children.reduce((acc, child) => {
|
|
219
|
+
if (child.type === 'WordNode') {
|
|
220
|
+
const node = {
|
|
221
|
+
text: getWord(child),
|
|
222
|
+
start: child.position.start.offset,
|
|
223
|
+
end: child.position.end.offset
|
|
224
|
+
};
|
|
225
|
+
return acc.concat([node]);
|
|
226
|
+
} else {
|
|
227
|
+
return acc;
|
|
228
|
+
}
|
|
229
|
+
}, acc);
|
|
230
|
+
} else {
|
|
231
|
+
return acc;
|
|
232
|
+
}
|
|
233
|
+
}, acc);
|
|
234
|
+
} else {
|
|
235
|
+
return acc;
|
|
236
|
+
}
|
|
237
|
+
}, []);
|
|
238
|
+
return out;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
class Intersection {
|
|
242
|
+
constructor(results) {
|
|
243
|
+
this.results = results;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
get hasOverlap() {
|
|
247
|
+
return this.results.filter(r => r.type === 'overlap').length > 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get surroundedTokens() {
|
|
251
|
+
return this.results.filter(r => r.type === 'within-selection').map(t => t.token);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* get intersection info for the selection in relation to tokens.
|
|
257
|
+
* @param {{start: number, end: number}} selection
|
|
258
|
+
* @param {{start: number, end: number}[]} tokens
|
|
259
|
+
* @return {tokens: [], type: 'overlap|no-overlap|contains'}
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
const intersection = (selection, tokens) => {
|
|
264
|
+
const {
|
|
265
|
+
start,
|
|
266
|
+
end
|
|
267
|
+
} = selection;
|
|
268
|
+
|
|
269
|
+
const startsWithin = t => start >= t.start && start < t.end;
|
|
270
|
+
|
|
271
|
+
const endsWithin = t => end > t.start && end <= t.end;
|
|
272
|
+
|
|
273
|
+
const mapped = tokens.map(t => {
|
|
274
|
+
if (start === t.start && end === t.end) {
|
|
275
|
+
return {
|
|
276
|
+
token: t,
|
|
277
|
+
type: 'exact-fit'
|
|
278
|
+
};
|
|
279
|
+
} else if (start <= t.start && end >= t.end) {
|
|
280
|
+
return {
|
|
281
|
+
token: t,
|
|
282
|
+
type: 'within-selection'
|
|
283
|
+
};
|
|
284
|
+
} else if (startsWithin(t) || endsWithin(t)) {
|
|
285
|
+
return {
|
|
286
|
+
token: t,
|
|
287
|
+
type: 'overlap'
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return new Intersection(compact(mapped));
|
|
292
|
+
};
|
|
293
|
+
const sort = tokens => {
|
|
294
|
+
if (!Array.isArray(tokens)) {
|
|
295
|
+
return tokens;
|
|
296
|
+
} else {
|
|
297
|
+
const out = clone(tokens);
|
|
298
|
+
out.sort((a, b) => {
|
|
299
|
+
const s = a.start < b.start ? -1 : a.start > b.start ? 1 : 0;
|
|
300
|
+
const e = a.end < b.end ? -1 : a.end > b.end ? 1 : 0;
|
|
301
|
+
|
|
302
|
+
if (s === -1 && e !== -1) {
|
|
303
|
+
throw new Error(`sort does not support intersecting tokens. a: ${a.start}-${a.end}, b: ${b.start}-${b.end}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return s;
|
|
307
|
+
});
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const normalize = (textToNormalize, tokens) => {
|
|
312
|
+
// making sure text provided is a string
|
|
313
|
+
const text = textToNormalize || '';
|
|
314
|
+
|
|
315
|
+
if (!Array.isArray(tokens) || tokens.length === 0) {
|
|
316
|
+
return [{
|
|
317
|
+
text,
|
|
318
|
+
start: 0,
|
|
319
|
+
end: text.length
|
|
320
|
+
}];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const out = sort(tokens).reduce((acc, t, index, outer) => {
|
|
324
|
+
let tokens = [];
|
|
325
|
+
const lastIndex = acc.lastIndex;
|
|
326
|
+
|
|
327
|
+
if (t.start === lastIndex) {
|
|
328
|
+
tokens = [{
|
|
329
|
+
text: text.substring(lastIndex, t.end),
|
|
330
|
+
start: lastIndex,
|
|
331
|
+
end: t.end,
|
|
332
|
+
predefined: true,
|
|
333
|
+
correct: t.correct,
|
|
334
|
+
isMissing: t.isMissing
|
|
335
|
+
}];
|
|
336
|
+
} else if (lastIndex < t.start) {
|
|
337
|
+
tokens = [{
|
|
338
|
+
text: text.substring(lastIndex, t.start),
|
|
339
|
+
start: lastIndex,
|
|
340
|
+
end: t.start
|
|
341
|
+
}, {
|
|
342
|
+
text: text.substring(t.start, t.end),
|
|
343
|
+
start: t.start,
|
|
344
|
+
end: t.end,
|
|
345
|
+
predefined: true,
|
|
346
|
+
correct: t.correct,
|
|
347
|
+
isMissing: t.isMissing
|
|
348
|
+
}];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (index === outer.length - 1 && t.end < text.length) {
|
|
352
|
+
const last = {
|
|
353
|
+
text: text.substring(t.end),
|
|
354
|
+
start: t.end,
|
|
355
|
+
end: text.length
|
|
356
|
+
};
|
|
357
|
+
tokens.push(last);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
lastIndex: tokens.length ? tokens[tokens.length - 1].end : lastIndex,
|
|
362
|
+
result: acc.result.concat(tokens)
|
|
363
|
+
};
|
|
364
|
+
}, {
|
|
365
|
+
result: [],
|
|
366
|
+
lastIndex: 0
|
|
367
|
+
});
|
|
368
|
+
return out.result;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const clearSelection = () => {
|
|
372
|
+
if (document.getSelection) {
|
|
373
|
+
// for all new browsers (IE9+, Chrome, Firefox)
|
|
374
|
+
document.getSelection().removeAllRanges();
|
|
375
|
+
document.getSelection().addRange(document.createRange());
|
|
376
|
+
} else if (window.getSelection) {
|
|
377
|
+
// equals with the document.getSelection (MSDN info)
|
|
378
|
+
if (window.getSelection().removeAllRanges) {
|
|
379
|
+
// for all new browsers (IE9+, Chrome, Firefox)
|
|
380
|
+
window.getSelection().removeAllRanges();
|
|
381
|
+
window.getSelection().addRange(document.createRange());
|
|
382
|
+
} else if (window.getSelection().empty) {
|
|
383
|
+
// Chrome supports this as well
|
|
384
|
+
window.getSelection().empty();
|
|
385
|
+
}
|
|
386
|
+
} else if (document.selection) {
|
|
387
|
+
// IE8-
|
|
388
|
+
document.selection.empty();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const getCaretCharacterOffsetWithin = element => {
|
|
392
|
+
var caretOffset = 0;
|
|
393
|
+
var doc = element.ownerDocument || element.document;
|
|
394
|
+
var win = doc.defaultView || doc.parentWindow;
|
|
395
|
+
var sel;
|
|
396
|
+
|
|
397
|
+
if (typeof win.getSelection !== 'undefined') {
|
|
398
|
+
sel = win.getSelection();
|
|
399
|
+
|
|
400
|
+
if (sel.rangeCount > 0) {
|
|
401
|
+
var range = win.getSelection().getRangeAt(0);
|
|
402
|
+
var selected = range.toString().length;
|
|
403
|
+
var preCaretRange = range.cloneRange();
|
|
404
|
+
preCaretRange.selectNodeContents(element);
|
|
405
|
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
406
|
+
|
|
407
|
+
if (selected) {
|
|
408
|
+
caretOffset = preCaretRange.toString().length - selected;
|
|
409
|
+
} else {
|
|
410
|
+
caretOffset = preCaretRange.toString().length;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} else if ((sel = doc.selection) && sel.type !== 'Control') {
|
|
414
|
+
var textRange = sel.createRange();
|
|
415
|
+
var preCaretTextRange = doc.body.createTextRange();
|
|
416
|
+
preCaretTextRange.moveToElementText(element);
|
|
417
|
+
preCaretTextRange.setEndPoint('EndToEnd', textRange);
|
|
418
|
+
caretOffset = preCaretTextRange.text.length;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return caretOffset;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const log$2 = debug('@pie-lib:text-select:token-text');
|
|
425
|
+
const Text = withStyles(() => ({
|
|
426
|
+
predefined: {
|
|
427
|
+
cursor: 'pointer',
|
|
428
|
+
backgroundColor: yellow[100],
|
|
429
|
+
border: `dashed 0px ${yellow[700]}`,
|
|
430
|
+
// we need this for nested tokenized elements like paragraphs, where p is inside span
|
|
431
|
+
'& *': {
|
|
432
|
+
cursor: 'pointer',
|
|
433
|
+
backgroundColor: yellow[100],
|
|
434
|
+
border: `dashed 0px ${yellow[700]}`
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
correct: {
|
|
438
|
+
backgroundColor: green[500],
|
|
439
|
+
'& *': {
|
|
440
|
+
backgroundColor: green[500]
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}))(({
|
|
444
|
+
text,
|
|
445
|
+
predefined,
|
|
446
|
+
classes,
|
|
447
|
+
onClick,
|
|
448
|
+
correct
|
|
449
|
+
}) => {
|
|
450
|
+
const formattedText = (text || '').replace(/\n/g, '<br>');
|
|
451
|
+
|
|
452
|
+
if (predefined) {
|
|
453
|
+
const className = classNames(classes.predefined, correct && classes.correct);
|
|
454
|
+
return /*#__PURE__*/React.createElement("span", {
|
|
455
|
+
onClick: onClick,
|
|
456
|
+
className: className,
|
|
457
|
+
dangerouslySetInnerHTML: {
|
|
458
|
+
__html: formattedText
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
return /*#__PURE__*/React.createElement("span", {
|
|
463
|
+
dangerouslySetInnerHTML: {
|
|
464
|
+
__html: formattedText
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
const notAllowedCharacters = ['\n', ' ', '\t'];
|
|
470
|
+
class TokenText extends React.Component {
|
|
471
|
+
constructor(...args) {
|
|
472
|
+
super(...args);
|
|
473
|
+
|
|
474
|
+
this.onClick = event => {
|
|
475
|
+
const {
|
|
476
|
+
onSelectToken,
|
|
477
|
+
text,
|
|
478
|
+
tokens
|
|
479
|
+
} = this.props;
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
|
|
482
|
+
if (typeof window === 'undefined') {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const selection = window.getSelection();
|
|
487
|
+
const textSelected = selection.toString();
|
|
488
|
+
|
|
489
|
+
if (textSelected.length > 0 && notAllowedCharacters.indexOf(textSelected) < 0) {
|
|
490
|
+
if (this.root) {
|
|
491
|
+
let offset = getCaretCharacterOffsetWithin(this.root);
|
|
492
|
+
/*
|
|
493
|
+
Since we implemented new line functionality (\n) using <br /> dom elements
|
|
494
|
+
and window.getSelection is not taking that into consideration, the offset might
|
|
495
|
+
be off by a few characters.
|
|
496
|
+
To combat that, we check if the selected text is right at the beginning of the offset.
|
|
497
|
+
If it's not, we add the additional offset in order for that to be accurate
|
|
498
|
+
*/
|
|
499
|
+
|
|
500
|
+
const newLineOffset = text.slice(offset).indexOf(textSelected);
|
|
501
|
+
offset += newLineOffset;
|
|
502
|
+
|
|
503
|
+
if (offset !== undefined) {
|
|
504
|
+
const endIndex = offset + textSelected.length;
|
|
505
|
+
|
|
506
|
+
if (endIndex <= text.length) {
|
|
507
|
+
const i = intersection({
|
|
508
|
+
start: offset,
|
|
509
|
+
end: endIndex
|
|
510
|
+
}, tokens);
|
|
511
|
+
|
|
512
|
+
if (i.hasOverlap) {
|
|
513
|
+
log$2('hasOverlap - do nothing');
|
|
514
|
+
clearSelection();
|
|
515
|
+
} else {
|
|
516
|
+
const tokensToRemove = i.surroundedTokens;
|
|
517
|
+
const token = {
|
|
518
|
+
text: textSelected,
|
|
519
|
+
start: offset,
|
|
520
|
+
end: endIndex
|
|
521
|
+
};
|
|
522
|
+
onSelectToken(token, tokensToRemove);
|
|
523
|
+
clearSelection();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
render() {
|
|
533
|
+
const {
|
|
534
|
+
text,
|
|
535
|
+
tokens,
|
|
536
|
+
className,
|
|
537
|
+
onTokenClick
|
|
538
|
+
} = this.props;
|
|
539
|
+
const normalized = normalize(text, tokens);
|
|
540
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
541
|
+
className: className,
|
|
542
|
+
ref: r => this.root = r,
|
|
543
|
+
onClick: this.onClick
|
|
544
|
+
}, normalized.map((t, index) => {
|
|
545
|
+
return /*#__PURE__*/React.createElement(Text, _extends({
|
|
546
|
+
key: index
|
|
547
|
+
}, t, {
|
|
548
|
+
onClick: () => onTokenClick(t)
|
|
549
|
+
}));
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
}
|
|
554
|
+
TokenText.propTypes = {
|
|
555
|
+
text: PropTypes.string.isRequired,
|
|
556
|
+
tokens: PropTypes.array.isRequired,
|
|
557
|
+
onTokenClick: PropTypes.func.isRequired,
|
|
558
|
+
onSelectToken: PropTypes.func.isRequired,
|
|
559
|
+
className: PropTypes.string
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
class Tokenizer extends React.Component {
|
|
563
|
+
constructor(props) {
|
|
564
|
+
super(props);
|
|
565
|
+
|
|
566
|
+
this.onChangeHandler = (token, mode) => {
|
|
567
|
+
this.props.onChange(token, mode);
|
|
568
|
+
this.setState({
|
|
569
|
+
mode
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
this.toggleCorrectMode = () => this.setState({
|
|
574
|
+
setCorrectMode: !this.state.setCorrectMode
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
this.clear = () => {
|
|
578
|
+
this.onChangeHandler([], '');
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
this.buildTokens = (type, fn) => {
|
|
582
|
+
const {
|
|
583
|
+
text
|
|
584
|
+
} = this.props;
|
|
585
|
+
const tokens = fn(text);
|
|
586
|
+
this.onChangeHandler(tokens, type);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
this.selectToken = (newToken, tokensToRemove) => {
|
|
590
|
+
const {
|
|
591
|
+
tokens
|
|
592
|
+
} = this.props;
|
|
593
|
+
const update = differenceWith(clone(tokens), tokensToRemove, isEqual);
|
|
594
|
+
update.push(newToken);
|
|
595
|
+
this.onChangeHandler(update, this.state.mode);
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
this.tokenClick = token => {
|
|
599
|
+
const {
|
|
600
|
+
setCorrectMode
|
|
601
|
+
} = this.state;
|
|
602
|
+
|
|
603
|
+
if (setCorrectMode) {
|
|
604
|
+
this.setCorrect(token);
|
|
605
|
+
} else {
|
|
606
|
+
this.removeToken(token);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
this.tokenIndex = token => {
|
|
611
|
+
const {
|
|
612
|
+
tokens
|
|
613
|
+
} = this.props;
|
|
614
|
+
return tokens.findIndex(t => {
|
|
615
|
+
return t.text == token.text && t.start == token.start && t.end == token.end;
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
this.setCorrect = token => {
|
|
620
|
+
const {
|
|
621
|
+
tokens
|
|
622
|
+
} = this.props;
|
|
623
|
+
const index = this.tokenIndex(token);
|
|
624
|
+
|
|
625
|
+
if (index !== -1) {
|
|
626
|
+
const t = tokens[index];
|
|
627
|
+
t.correct = !t.correct;
|
|
628
|
+
const update = clone(tokens);
|
|
629
|
+
update.splice(index, 1, t);
|
|
630
|
+
this.onChangeHandler(update, this.state.mode);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
this.removeToken = token => {
|
|
635
|
+
const {
|
|
636
|
+
tokens
|
|
637
|
+
} = this.props;
|
|
638
|
+
const index = this.tokenIndex(token);
|
|
639
|
+
|
|
640
|
+
if (index !== -1) {
|
|
641
|
+
const update = clone(tokens);
|
|
642
|
+
update.splice(index, 1);
|
|
643
|
+
this.onChangeHandler(update, this.state.mode);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
this.state = {
|
|
648
|
+
setCorrectMode: false,
|
|
649
|
+
mode: ''
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
render() {
|
|
654
|
+
const {
|
|
655
|
+
text,
|
|
656
|
+
tokens,
|
|
657
|
+
classes,
|
|
658
|
+
className
|
|
659
|
+
} = this.props;
|
|
660
|
+
const {
|
|
661
|
+
setCorrectMode
|
|
662
|
+
} = this.state;
|
|
663
|
+
const tokenClassName = classNames(classes.text, setCorrectMode && classes.noselect);
|
|
664
|
+
const rootName = classNames(classes.tokenizer, className);
|
|
665
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
666
|
+
className: rootName
|
|
667
|
+
}, /*#__PURE__*/React.createElement(Controls$1, {
|
|
668
|
+
onClear: this.clear,
|
|
669
|
+
onWords: () => this.buildTokens('words', words),
|
|
670
|
+
onSentences: () => this.buildTokens('sentence', sentences),
|
|
671
|
+
onParagraphs: () => this.buildTokens('paragraphs', paragraphs),
|
|
672
|
+
setCorrectMode: setCorrectMode,
|
|
673
|
+
onToggleCorrectMode: this.toggleCorrectMode
|
|
674
|
+
}), /*#__PURE__*/React.createElement(TokenText, {
|
|
675
|
+
className: tokenClassName,
|
|
676
|
+
text: text,
|
|
677
|
+
tokens: tokens,
|
|
678
|
+
onTokenClick: this.tokenClick,
|
|
679
|
+
onSelectToken: this.selectToken
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
}
|
|
684
|
+
Tokenizer.propTypes = {
|
|
685
|
+
text: PropTypes.string.isRequired,
|
|
686
|
+
tokens: PropTypes.arrayOf(PropTypes.shape({
|
|
687
|
+
text: PropTypes.string,
|
|
688
|
+
correct: PropTypes.bool,
|
|
689
|
+
start: PropTypes.number,
|
|
690
|
+
end: PropTypes.number
|
|
691
|
+
})),
|
|
692
|
+
classes: PropTypes.object.isRequired,
|
|
693
|
+
className: PropTypes.string,
|
|
694
|
+
onChange: PropTypes.func.isRequired
|
|
695
|
+
};
|
|
696
|
+
Tokenizer.defaultProps = {};
|
|
697
|
+
var index = withStyles(() => ({
|
|
698
|
+
text: {
|
|
699
|
+
whiteSpace: 'pre-wrap'
|
|
700
|
+
},
|
|
701
|
+
noselect: _extends({}, noSelect())
|
|
702
|
+
}))(Tokenizer);
|
|
703
|
+
|
|
704
|
+
const LINE_HEIGHT_MULTIPLIER = 3.2; // we need a bit more space for correctness indicators
|
|
705
|
+
|
|
706
|
+
const CORRECTNESS_LINE_HEIGHT_MULTIPLIER = 3.4;
|
|
707
|
+
const CORRECTNESS_PADDING = 2;
|
|
708
|
+
|
|
709
|
+
const Wrapper = ({
|
|
710
|
+
useWrapper,
|
|
711
|
+
children,
|
|
712
|
+
classNameContainer,
|
|
713
|
+
iconClass,
|
|
714
|
+
Icon
|
|
715
|
+
}) => useWrapper ? /*#__PURE__*/React.createElement("span", {
|
|
716
|
+
className: classNameContainer
|
|
717
|
+
}, children, /*#__PURE__*/React.createElement(Icon, {
|
|
718
|
+
className: iconClass
|
|
719
|
+
})) : children;
|
|
720
|
+
|
|
721
|
+
Wrapper.propTypes = {
|
|
722
|
+
useWrapper: PropTypes.bool,
|
|
723
|
+
classNameContainer: PropTypes.string,
|
|
724
|
+
iconClass: PropTypes.string,
|
|
725
|
+
Icon: PropTypes.func,
|
|
726
|
+
children: PropTypes.element
|
|
727
|
+
};
|
|
728
|
+
const TokenTypes = {
|
|
729
|
+
text: PropTypes.string,
|
|
730
|
+
selectable: PropTypes.bool
|
|
731
|
+
};
|
|
732
|
+
class Token extends React.Component {
|
|
733
|
+
constructor(...args) {
|
|
734
|
+
super(...args);
|
|
735
|
+
|
|
736
|
+
this.getClassAndIconConfig = () => {
|
|
737
|
+
const {
|
|
738
|
+
selectable,
|
|
739
|
+
selected,
|
|
740
|
+
classes,
|
|
741
|
+
className: classNameProp,
|
|
742
|
+
disabled,
|
|
743
|
+
highlight,
|
|
744
|
+
correct,
|
|
745
|
+
animationsDisabled,
|
|
746
|
+
isMissing
|
|
747
|
+
} = this.props;
|
|
748
|
+
const isTouchEnabled = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
749
|
+
const baseClassName = Token.rootClassName;
|
|
750
|
+
let classNameContainer;
|
|
751
|
+
let Icon;
|
|
752
|
+
let iconClass;
|
|
753
|
+
|
|
754
|
+
if (correct === undefined && selected && disabled) {
|
|
755
|
+
return {
|
|
756
|
+
className: classNames(classes.token, classes.selected, classes.disabledBlack)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (correct !== undefined) {
|
|
761
|
+
const isCorrect = correct === true;
|
|
762
|
+
return {
|
|
763
|
+
className: classNames(baseClassName, classes.custom),
|
|
764
|
+
classNameContainer: classNames(isCorrect ? classes.correct : classes.incorrect, classes.commonTokenStyle),
|
|
765
|
+
Icon: isCorrect ? Check : Close,
|
|
766
|
+
iconClass: classNames(classes.correctnessIndicatorIcon, isCorrect ? classes.correctIcon : classes.incorrectIcon)
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (isMissing) {
|
|
771
|
+
return {
|
|
772
|
+
className: classNames(baseClassName, classes.custom, classes.missing, classes.commonTokenStyle),
|
|
773
|
+
classNameContainer: classes.commonTokenStyle,
|
|
774
|
+
Icon: Close,
|
|
775
|
+
iconClass: classNames(classes.correctnessIndicatorIcon, classes.incorrectIcon)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
className: classNames(baseClassName, classes.token, disabled && classes.disabled, selectable && !disabled && !isTouchEnabled && classes.selectable, selected && !disabled && classes.selected, selected && disabled && classes.disabledAndSelected, highlight && selectable && !disabled && !selected && classes.highlight, animationsDisabled && classes.print, classNameProp),
|
|
781
|
+
classNameContainer,
|
|
782
|
+
Icon,
|
|
783
|
+
iconClass
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
render() {
|
|
789
|
+
const {
|
|
790
|
+
text,
|
|
791
|
+
index,
|
|
792
|
+
correct,
|
|
793
|
+
isMissing
|
|
794
|
+
} = this.props;
|
|
795
|
+
const {
|
|
796
|
+
className,
|
|
797
|
+
classNameContainer,
|
|
798
|
+
Icon,
|
|
799
|
+
iconClass
|
|
800
|
+
} = this.getClassAndIconConfig();
|
|
801
|
+
return /*#__PURE__*/React.createElement(Wrapper, {
|
|
802
|
+
useWrapper: correct !== undefined || isMissing,
|
|
803
|
+
classNameContainer: classNameContainer,
|
|
804
|
+
iconClass: iconClass,
|
|
805
|
+
Icon: Icon
|
|
806
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
807
|
+
className: className,
|
|
808
|
+
dangerouslySetInnerHTML: {
|
|
809
|
+
__html: (text || '').replace(/\n/g, '<br>')
|
|
810
|
+
},
|
|
811
|
+
"data-indexkey": index
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
}
|
|
816
|
+
Token.rootClassName = 'tokenRootClass';
|
|
817
|
+
Token.propTypes = _extends({}, TokenTypes, {
|
|
818
|
+
classes: PropTypes.object.isRequired,
|
|
819
|
+
text: PropTypes.string.isRequired,
|
|
820
|
+
className: PropTypes.string,
|
|
821
|
+
disabled: PropTypes.bool,
|
|
822
|
+
highlight: PropTypes.bool,
|
|
823
|
+
correct: PropTypes.bool
|
|
824
|
+
});
|
|
825
|
+
Token.defaultProps = {
|
|
826
|
+
selectable: false,
|
|
827
|
+
text: ''
|
|
828
|
+
};
|
|
829
|
+
var Token$1 = withStyles(theme => {
|
|
830
|
+
return {
|
|
831
|
+
token: {
|
|
832
|
+
cursor: 'pointer',
|
|
833
|
+
textIndent: 0
|
|
834
|
+
},
|
|
835
|
+
disabled: {
|
|
836
|
+
cursor: 'inherit',
|
|
837
|
+
color: color.disabled()
|
|
838
|
+
},
|
|
839
|
+
disabledBlack: {
|
|
840
|
+
cursor: 'inherit'
|
|
841
|
+
},
|
|
842
|
+
disabledAndSelected: {
|
|
843
|
+
backgroundColor: color.blueGrey100()
|
|
844
|
+
},
|
|
845
|
+
selectable: {
|
|
846
|
+
[theme.breakpoints.up(769)]: {
|
|
847
|
+
'&:hover': {
|
|
848
|
+
backgroundColor: color.blueGrey300(),
|
|
849
|
+
color: theme.palette.common.black,
|
|
850
|
+
'& > *': {
|
|
851
|
+
backgroundColor: color.blueGrey300()
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
selected: {
|
|
857
|
+
backgroundColor: color.blueGrey100(),
|
|
858
|
+
color: theme.palette.common.black,
|
|
859
|
+
lineHeight: `${theme.spacing.unit * LINE_HEIGHT_MULTIPLIER}px`,
|
|
860
|
+
border: `solid 2px ${color.blueGrey900()}`,
|
|
861
|
+
borderRadius: '4px',
|
|
862
|
+
'& > *': {
|
|
863
|
+
backgroundColor: color.blueGrey100()
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
highlight: {
|
|
867
|
+
border: `dashed 2px ${color.blueGrey600()}`,
|
|
868
|
+
borderRadius: '4px',
|
|
869
|
+
lineHeight: `${theme.spacing.unit * LINE_HEIGHT_MULTIPLIER}px`
|
|
870
|
+
},
|
|
871
|
+
print: {
|
|
872
|
+
border: `dashed 2px ${color.blueGrey600()}`,
|
|
873
|
+
borderRadius: '4px',
|
|
874
|
+
lineHeight: `${theme.spacing.unit * LINE_HEIGHT_MULTIPLIER}px`,
|
|
875
|
+
color: color.text()
|
|
876
|
+
},
|
|
877
|
+
custom: {
|
|
878
|
+
display: 'initial'
|
|
879
|
+
},
|
|
880
|
+
commonTokenStyle: {
|
|
881
|
+
position: 'relative',
|
|
882
|
+
borderRadius: '4px',
|
|
883
|
+
color: theme.palette.common.black,
|
|
884
|
+
lineHeight: `${theme.spacing.unit * CORRECTNESS_LINE_HEIGHT_MULTIPLIER + CORRECTNESS_PADDING}px`,
|
|
885
|
+
padding: `${CORRECTNESS_PADDING}px`
|
|
886
|
+
},
|
|
887
|
+
correct: {
|
|
888
|
+
border: `${color.correctTertiary()} solid 2px`
|
|
889
|
+
},
|
|
890
|
+
incorrect: {
|
|
891
|
+
border: `${color.incorrectWithIcon()} solid 2px`
|
|
892
|
+
},
|
|
893
|
+
missing: {
|
|
894
|
+
border: `${color.incorrectWithIcon()} dashed 2px`
|
|
895
|
+
},
|
|
896
|
+
incorrectIcon: {
|
|
897
|
+
backgroundColor: color.incorrectWithIcon()
|
|
898
|
+
},
|
|
899
|
+
correctIcon: {
|
|
900
|
+
backgroundColor: color.correctTertiary()
|
|
901
|
+
},
|
|
902
|
+
correctnessIndicatorIcon: {
|
|
903
|
+
color: color.white(),
|
|
904
|
+
position: 'absolute',
|
|
905
|
+
top: '-8px',
|
|
906
|
+
left: '-8px',
|
|
907
|
+
borderRadius: '50%',
|
|
908
|
+
fontSize: '12px',
|
|
909
|
+
padding: '2px'
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
})(Token);
|
|
913
|
+
|
|
914
|
+
const log$1 = debug('@pie-lib:text-select:token-select');
|
|
915
|
+
class TokenSelect extends React.Component {
|
|
916
|
+
constructor(...args) {
|
|
917
|
+
super(...args);
|
|
918
|
+
|
|
919
|
+
this.selectedCount = () => this.props.tokens.filter(t => t.selected).length;
|
|
920
|
+
|
|
921
|
+
this.canSelectMore = selectedCount => {
|
|
922
|
+
const {
|
|
923
|
+
maxNoOfSelections
|
|
924
|
+
} = this.props;
|
|
925
|
+
|
|
926
|
+
if (maxNoOfSelections === 1) {
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
log$1('[canSelectMore] maxNoOfSelections: ', maxNoOfSelections, 'selectedCount: ', selectedCount);
|
|
931
|
+
return maxNoOfSelections <= 0 || isFinite(maxNoOfSelections) && selectedCount < maxNoOfSelections;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
this.toggleToken = event => {
|
|
935
|
+
const {
|
|
936
|
+
target
|
|
937
|
+
} = event;
|
|
938
|
+
const {
|
|
939
|
+
tokens,
|
|
940
|
+
animationsDisabled
|
|
941
|
+
} = this.props;
|
|
942
|
+
const tokensCloned = clone(tokens);
|
|
943
|
+
const targetSpanWrapper = target.closest(`.${Token$1.rootClassName}`);
|
|
944
|
+
const targetedTokenIndex = targetSpanWrapper && targetSpanWrapper.dataset && targetSpanWrapper.dataset.indexkey;
|
|
945
|
+
const t = targetedTokenIndex && tokensCloned[targetedTokenIndex]; // don't toggle if we are in print mode, token correctness is defined or if it's missing
|
|
946
|
+
// (missing means that it was evaluated as correct and not selected)
|
|
947
|
+
|
|
948
|
+
if (t && t.correct === undefined && !animationsDisabled && !t.isMissing) {
|
|
949
|
+
const {
|
|
950
|
+
onChange,
|
|
951
|
+
maxNoOfSelections
|
|
952
|
+
} = this.props;
|
|
953
|
+
const selected = !t.selected;
|
|
954
|
+
|
|
955
|
+
if (maxNoOfSelections === 1 && this.selectedCount() === 1) {
|
|
956
|
+
const selectedToken = (tokens || []).filter(t => t.selected);
|
|
957
|
+
const updatedTokens = tokensCloned.map(token => {
|
|
958
|
+
if (isEqual(token, selectedToken[0])) {
|
|
959
|
+
return _extends({}, token, {
|
|
960
|
+
selected: false
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return _extends({}, token, {
|
|
965
|
+
selectable: true
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const update = _extends({}, t, {
|
|
970
|
+
selected: !t.selected
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
updatedTokens.splice(targetedTokenIndex, 1, update);
|
|
974
|
+
onChange(updatedTokens);
|
|
975
|
+
} else {
|
|
976
|
+
if (selected && maxNoOfSelections > 0 && this.selectedCount() >= maxNoOfSelections) {
|
|
977
|
+
log$1('skip toggle max reached');
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const update = _extends({}, t, {
|
|
982
|
+
selected: !t.selected
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
tokensCloned.splice(targetedTokenIndex, 1, update);
|
|
986
|
+
onChange(tokensCloned);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
this.generateTokensInHtml = () => {
|
|
992
|
+
const {
|
|
993
|
+
tokens,
|
|
994
|
+
disabled,
|
|
995
|
+
highlightChoices,
|
|
996
|
+
animationsDisabled
|
|
997
|
+
} = this.props;
|
|
998
|
+
const selectedCount = this.selectedCount();
|
|
999
|
+
|
|
1000
|
+
const isLineBreak = text => text === '\n';
|
|
1001
|
+
|
|
1002
|
+
const isNewParagraph = text => text === '\n\n';
|
|
1003
|
+
|
|
1004
|
+
const reducer = (accumulator, t, index) => {
|
|
1005
|
+
const selectable = t.selected || t.selectable && this.canSelectMore(selectedCount);
|
|
1006
|
+
const showCorrectAnswer = t.correct !== undefined && (t.selectable || t.selected);
|
|
1007
|
+
let finalAcc = accumulator;
|
|
1008
|
+
|
|
1009
|
+
if (isNewParagraph(t.text)) {
|
|
1010
|
+
return finalAcc + '</p><p>';
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (isLineBreak(t.text)) {
|
|
1014
|
+
return finalAcc + '<br>';
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (selectable && !disabled || showCorrectAnswer || t.selected || t.isMissing || animationsDisabled && t.predefined // if we are in print mode
|
|
1018
|
+
) {
|
|
1019
|
+
return finalAcc + renderToString( /*#__PURE__*/React.createElement(Token$1, _extends({
|
|
1020
|
+
key: index,
|
|
1021
|
+
disabled: disabled,
|
|
1022
|
+
index: index
|
|
1023
|
+
}, t, {
|
|
1024
|
+
selectable: selectable,
|
|
1025
|
+
highlight: highlightChoices,
|
|
1026
|
+
animationsDisabled: animationsDisabled
|
|
1027
|
+
})));
|
|
1028
|
+
} else {
|
|
1029
|
+
return accumulator + t.text;
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const reduceResult = (tokens || []).reduce(reducer, '<p>');
|
|
1034
|
+
return reduceResult + '</p>';
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
render() {
|
|
1039
|
+
const {
|
|
1040
|
+
classes,
|
|
1041
|
+
className: classNameProp
|
|
1042
|
+
} = this.props;
|
|
1043
|
+
const className = classNames(classes.tokenSelect, classNameProp);
|
|
1044
|
+
const html = this.generateTokensInHtml();
|
|
1045
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1046
|
+
className: className,
|
|
1047
|
+
dangerouslySetInnerHTML: {
|
|
1048
|
+
__html: html
|
|
1049
|
+
},
|
|
1050
|
+
onClick: this.toggleToken
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
}
|
|
1055
|
+
TokenSelect.propTypes = {
|
|
1056
|
+
tokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
|
|
1057
|
+
className: PropTypes.string,
|
|
1058
|
+
classes: PropTypes.object.isRequired,
|
|
1059
|
+
onChange: PropTypes.func.isRequired,
|
|
1060
|
+
disabled: PropTypes.bool,
|
|
1061
|
+
highlightChoices: PropTypes.bool,
|
|
1062
|
+
animationsDisabled: PropTypes.bool,
|
|
1063
|
+
maxNoOfSelections: PropTypes.number
|
|
1064
|
+
};
|
|
1065
|
+
TokenSelect.defaultProps = {
|
|
1066
|
+
highlightChoices: false,
|
|
1067
|
+
maxNoOfSelections: 0,
|
|
1068
|
+
tokens: []
|
|
1069
|
+
};
|
|
1070
|
+
var TokenSelect$1 = withStyles(() => ({
|
|
1071
|
+
tokenSelect: _extends({
|
|
1072
|
+
backgroundColor: 'none',
|
|
1073
|
+
whiteSpace: 'pre'
|
|
1074
|
+
}, noSelect(), {
|
|
1075
|
+
'& p': {
|
|
1076
|
+
whiteSpace: 'break-spaces'
|
|
1077
|
+
}
|
|
1078
|
+
})
|
|
1079
|
+
}))(TokenSelect); // Re-export TokenTypes for external use
|
|
1080
|
+
|
|
1081
|
+
const log = debug('@pie-lib:text-select');
|
|
1082
|
+
/**
|
|
1083
|
+
* Built on TokenSelect uses build.normalize to build the token set.
|
|
1084
|
+
*/
|
|
1085
|
+
|
|
1086
|
+
class TextSelect extends React.Component {
|
|
1087
|
+
constructor(...args) {
|
|
1088
|
+
super(...args);
|
|
1089
|
+
|
|
1090
|
+
this.change = tokens => {
|
|
1091
|
+
const {
|
|
1092
|
+
onChange
|
|
1093
|
+
} = this.props;
|
|
1094
|
+
|
|
1095
|
+
if (!onChange) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const out = tokens.filter(t => t.selected).map(t => ({
|
|
1100
|
+
start: t.start,
|
|
1101
|
+
end: t.end
|
|
1102
|
+
}));
|
|
1103
|
+
onChange(out);
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
render() {
|
|
1108
|
+
const {
|
|
1109
|
+
text,
|
|
1110
|
+
disabled,
|
|
1111
|
+
tokens,
|
|
1112
|
+
selectedTokens,
|
|
1113
|
+
className,
|
|
1114
|
+
highlightChoices,
|
|
1115
|
+
maxNoOfSelections,
|
|
1116
|
+
animationsDisabled
|
|
1117
|
+
} = this.props;
|
|
1118
|
+
const normalized = normalize(text, tokens);
|
|
1119
|
+
log('normalized: ', normalized);
|
|
1120
|
+
const prepped = normalized.map(t => {
|
|
1121
|
+
const selectedIndex = selectedTokens.findIndex(s => {
|
|
1122
|
+
return s.start === t.start && s.end === t.end;
|
|
1123
|
+
});
|
|
1124
|
+
const selected = selectedIndex !== -1;
|
|
1125
|
+
const correct = selected ? t.correct : undefined;
|
|
1126
|
+
const isMissing = t.isMissing;
|
|
1127
|
+
return _extends({}, t, {
|
|
1128
|
+
selectable: !disabled && t.predefined,
|
|
1129
|
+
selected,
|
|
1130
|
+
correct,
|
|
1131
|
+
isMissing
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
return /*#__PURE__*/React.createElement(TokenSelect$1, {
|
|
1135
|
+
highlightChoices: !disabled && highlightChoices,
|
|
1136
|
+
className: className,
|
|
1137
|
+
tokens: prepped,
|
|
1138
|
+
disabled: disabled,
|
|
1139
|
+
onChange: this.change,
|
|
1140
|
+
maxNoOfSelections: maxNoOfSelections,
|
|
1141
|
+
animationsDisabled: animationsDisabled
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
}
|
|
1146
|
+
TextSelect.propTypes = {
|
|
1147
|
+
onChange: PropTypes.func,
|
|
1148
|
+
disabled: PropTypes.bool,
|
|
1149
|
+
tokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
|
|
1150
|
+
selectedTokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
|
|
1151
|
+
text: PropTypes.string.isRequired,
|
|
1152
|
+
className: PropTypes.string,
|
|
1153
|
+
highlightChoices: PropTypes.bool,
|
|
1154
|
+
animationsDisabled: PropTypes.bool,
|
|
1155
|
+
maxNoOfSelections: PropTypes.number
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const {
|
|
1159
|
+
translator
|
|
1160
|
+
} = Translator;
|
|
1161
|
+
const Legend = withStyles(theme => ({
|
|
1162
|
+
flexContainer: {
|
|
1163
|
+
display: 'flex',
|
|
1164
|
+
flexDirection: 'row',
|
|
1165
|
+
alignItems: 'center',
|
|
1166
|
+
gap: `${2 * theme.spacing.unit}px`,
|
|
1167
|
+
borderBottom: '1px solid lightgrey',
|
|
1168
|
+
borderTop: '1px solid lightgrey',
|
|
1169
|
+
paddingBottom: theme.spacing.unit,
|
|
1170
|
+
paddingTop: theme.spacing.unit,
|
|
1171
|
+
marginBottom: theme.spacing.unit
|
|
1172
|
+
},
|
|
1173
|
+
key: {
|
|
1174
|
+
fontSize: '14px',
|
|
1175
|
+
fontWeight: 'bold',
|
|
1176
|
+
color: color.black(),
|
|
1177
|
+
marginLeft: theme.spacing.unit
|
|
1178
|
+
},
|
|
1179
|
+
container: {
|
|
1180
|
+
position: 'relative',
|
|
1181
|
+
padding: '4px',
|
|
1182
|
+
fontSize: '14px',
|
|
1183
|
+
borderRadius: '4px'
|
|
1184
|
+
},
|
|
1185
|
+
correct: {
|
|
1186
|
+
border: `${color.correctTertiary()} solid 2px`
|
|
1187
|
+
},
|
|
1188
|
+
incorrect: {
|
|
1189
|
+
border: `${color.incorrectWithIcon()} solid 2px`
|
|
1190
|
+
},
|
|
1191
|
+
missing: {
|
|
1192
|
+
border: `${color.incorrectWithIcon()} dashed 2px`
|
|
1193
|
+
},
|
|
1194
|
+
incorrectIcon: {
|
|
1195
|
+
backgroundColor: color.incorrectWithIcon()
|
|
1196
|
+
},
|
|
1197
|
+
correctIcon: {
|
|
1198
|
+
backgroundColor: color.correctTertiary()
|
|
1199
|
+
},
|
|
1200
|
+
icon: {
|
|
1201
|
+
color: color.white(),
|
|
1202
|
+
position: 'absolute',
|
|
1203
|
+
top: '-8px',
|
|
1204
|
+
left: '-8px',
|
|
1205
|
+
borderRadius: '50%',
|
|
1206
|
+
fontSize: '12px',
|
|
1207
|
+
padding: '2px'
|
|
1208
|
+
}
|
|
1209
|
+
}))(({
|
|
1210
|
+
classes,
|
|
1211
|
+
language,
|
|
1212
|
+
showOnlyCorrect
|
|
1213
|
+
}) => {
|
|
1214
|
+
const legendItems = [{
|
|
1215
|
+
Icon: Check,
|
|
1216
|
+
label: translator.t('selectText.correctAnswerSelected', {
|
|
1217
|
+
lng: language
|
|
1218
|
+
}),
|
|
1219
|
+
containerClass: classNames(classes.correct, classes.container),
|
|
1220
|
+
iconClass: classNames(classes.correctIcon, classes.icon)
|
|
1221
|
+
}, {
|
|
1222
|
+
Icon: Close,
|
|
1223
|
+
label: translator.t('selectText.incorrectSelection', {
|
|
1224
|
+
lng: language
|
|
1225
|
+
}),
|
|
1226
|
+
containerClass: classNames(classes.incorrect, classes.container),
|
|
1227
|
+
iconClass: classNames(classes.incorrectIcon, classes.icon)
|
|
1228
|
+
}, {
|
|
1229
|
+
Icon: Close,
|
|
1230
|
+
label: translator.t('selectText.correctAnswerNotSelected', {
|
|
1231
|
+
lng: language
|
|
1232
|
+
}),
|
|
1233
|
+
containerClass: classNames(classes.missing, classes.container),
|
|
1234
|
+
iconClass: classNames(classes.incorrectIcon, classes.icon)
|
|
1235
|
+
}];
|
|
1236
|
+
|
|
1237
|
+
if (showOnlyCorrect) {
|
|
1238
|
+
legendItems.splice(1, 2);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1242
|
+
className: classes.flexContainer
|
|
1243
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1244
|
+
className: classes.key
|
|
1245
|
+
}, translator.t('selectText.key', {
|
|
1246
|
+
lng: language
|
|
1247
|
+
})), legendItems.map(({
|
|
1248
|
+
Icon,
|
|
1249
|
+
label,
|
|
1250
|
+
containerClass,
|
|
1251
|
+
iconClass
|
|
1252
|
+
}, idx) => /*#__PURE__*/React.createElement("div", {
|
|
1253
|
+
key: idx,
|
|
1254
|
+
className: containerClass
|
|
1255
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
1256
|
+
className: iconClass
|
|
1257
|
+
}), /*#__PURE__*/React.createElement("span", null, label))));
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
const createElementFromHTML = (htmlString = '') => {
|
|
1261
|
+
const div = document.createElement('div');
|
|
1262
|
+
div.innerHTML = htmlString.trim();
|
|
1263
|
+
return div;
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
const parseBrs = dom => {
|
|
1267
|
+
const brs = dom.querySelectorAll('br');
|
|
1268
|
+
brs.forEach(br => br.replaceWith('\n'));
|
|
1269
|
+
dom.innerHTML = dom.innerHTML.replace(/\n\n/g, '\n');
|
|
1270
|
+
};
|
|
1271
|
+
const parseParagraph = (paragraph, end) => {
|
|
1272
|
+
if (end) {
|
|
1273
|
+
return paragraph.innerHTML;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return `${paragraph.innerHTML}\n\n`;
|
|
1277
|
+
};
|
|
1278
|
+
const parseParagraphs = dom => {
|
|
1279
|
+
const paragraphs = dom.querySelectorAll('p'); // separate variable for easily debugging, if needed
|
|
1280
|
+
|
|
1281
|
+
let str = '';
|
|
1282
|
+
paragraphs.forEach((par, index) => {
|
|
1283
|
+
str += parseParagraph(par, index === paragraphs.length - 1);
|
|
1284
|
+
});
|
|
1285
|
+
return str || null;
|
|
1286
|
+
};
|
|
1287
|
+
const prepareText = text => {
|
|
1288
|
+
let txtDom = createElementFromHTML(text);
|
|
1289
|
+
const allDomElements = Array.from(txtDom.querySelectorAll('*'));
|
|
1290
|
+
|
|
1291
|
+
if (txtDom.querySelectorAll('p').length === 0) {
|
|
1292
|
+
const div = document.createElement('div');
|
|
1293
|
+
div.innerHTML = `<p>${txtDom.innerHTML}</p>`;
|
|
1294
|
+
txtDom = div;
|
|
1295
|
+
} // if no dom elements, we just return the text
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
if (allDomElements.length === 0) {
|
|
1299
|
+
return text;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
parseBrs(txtDom);
|
|
1303
|
+
return parseParagraphs(txtDom);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
export { Legend, TextSelect, Token$1 as Token, TokenSelect$1 as TokenSelect, TokenTypes, index as Tokenizer, prepareText };
|
|
1307
|
+
//# sourceMappingURL=index.js.map
|