@pie-lib/text-select 2.1.1-next.0 → 2.2.0-next.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.
- package/lib/index.js +0 -7
- package/lib/index.js.map +1 -1
- package/lib/legend.js +0 -12
- package/lib/legend.js.map +1 -1
- package/lib/text-select.js +0 -14
- package/lib/text-select.js.map +1 -1
- package/lib/token-select/index.js +3 -24
- package/lib/token-select/index.js.map +1 -1
- package/lib/token-select/token.js +0 -17
- package/lib/token-select/token.js.map +1 -1
- package/lib/tokenizer/builder.js +3 -14
- package/lib/tokenizer/builder.js.map +1 -1
- package/lib/tokenizer/controls.js +0 -2
- package/lib/tokenizer/controls.js.map +1 -1
- package/lib/tokenizer/index.js +4 -19
- package/lib/tokenizer/index.js.map +1 -1
- package/lib/tokenizer/selection-utils.js +0 -4
- package/lib/tokenizer/selection-utils.js.map +1 -1
- package/lib/tokenizer/token-text.js +0 -11
- package/lib/tokenizer/token-text.js.map +1 -1
- package/lib/utils.js +0 -9
- package/lib/utils.js.map +1 -1
- package/package.json +11 -8
- package/src/__tests__/legend.test.jsx +211 -0
- package/src/token-select/__tests__/index.test.jsx +264 -2
- package/src/token-select/__tests__/token.test.jsx +207 -1
- package/src/token-select/index.jsx +1 -2
- package/src/token-select/token.jsx +1 -5
- package/src/tokenizer/__tests__/builder.test.js +1 -1
- package/src/tokenizer/__tests__/controls.test.jsx +1 -1
- package/src/tokenizer/__tests__/index.test.jsx +289 -7
- package/src/tokenizer/__tests__/selection-utils.test.js +122 -3
- package/src/tokenizer/__tests__/token-text.test.jsx +285 -9
- package/src/tokenizer/builder.js +2 -3
- package/src/tokenizer/controls.jsx +3 -18
- package/src/tokenizer/index.jsx +2 -4
- package/src/tokenizer/token-text.jsx +2 -2
- package/NEXT.CHANGELOG.json +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import TokenText from '../token-text';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import TokenText, { Text } from '../token-text';
|
|
4
4
|
|
|
5
5
|
const tokens = () => [
|
|
6
6
|
{
|
|
7
7
|
start: 0,
|
|
8
|
-
end:
|
|
9
|
-
text:
|
|
8
|
+
end: 5,
|
|
9
|
+
text: 'lorem',
|
|
10
10
|
},
|
|
11
11
|
];
|
|
12
12
|
|
|
@@ -14,10 +14,19 @@ describe('token-text', () => {
|
|
|
14
14
|
const defaultProps = {
|
|
15
15
|
onTokenClick: jest.fn(),
|
|
16
16
|
onSelectToken: jest.fn(),
|
|
17
|
-
text: `lorem
|
|
17
|
+
text: `lorem ipsum dolor`,
|
|
18
18
|
tokens: tokens(),
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
if (typeof global.window !== 'undefined') {
|
|
24
|
+
global.window.getSelection = jest.fn().mockReturnValue({
|
|
25
|
+
toString: () => '',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
21
30
|
describe('rendering', () => {
|
|
22
31
|
it('renders with text and tokens', () => {
|
|
23
32
|
const { container } = render(<TokenText {...defaultProps} />);
|
|
@@ -33,10 +42,277 @@ describe('token-text', () => {
|
|
|
33
42
|
const { container } = render(<TokenText {...defaultProps} text="Line 1\nLine 2\nLine 3" />);
|
|
34
43
|
expect(container.firstChild).toBeInTheDocument();
|
|
35
44
|
});
|
|
45
|
+
|
|
46
|
+
it('renders with custom className', () => {
|
|
47
|
+
const { container } = render(<TokenText {...defaultProps} className="custom-class" />);
|
|
48
|
+
expect(container.firstChild).toHaveClass('custom-class');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders normalized tokens', () => {
|
|
52
|
+
const { container } = render(<TokenText {...defaultProps} />);
|
|
53
|
+
const spans = container.querySelectorAll('span');
|
|
54
|
+
expect(spans.length).toBeGreaterThan(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Text component', () => {
|
|
59
|
+
it('renders plain text when not predefined', () => {
|
|
60
|
+
const { container } = render(<Text text="hello" predefined={false} />);
|
|
61
|
+
expect(container.querySelector('span')).toBeInTheDocument();
|
|
62
|
+
expect(container.querySelector('span')).not.toHaveClass('predefined');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders predefined text with correct class', () => {
|
|
66
|
+
const { container } = render(<Text text="hello" predefined={true} onClick={jest.fn()} />);
|
|
67
|
+
expect(container.querySelector('span')).toHaveClass('predefined');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders correct text with both classes', () => {
|
|
71
|
+
const { container } = render(<Text text="hello" predefined={true} correct={true} onClick={jest.fn()} />);
|
|
72
|
+
const span = container.querySelector('span');
|
|
73
|
+
expect(span).toHaveClass('predefined');
|
|
74
|
+
expect(span).toHaveClass('correct');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('calls onClick when predefined text is clicked', () => {
|
|
78
|
+
const onClick = jest.fn();
|
|
79
|
+
const { container } = render(<Text text="hello" predefined={true} onClick={onClick} />);
|
|
80
|
+
const span = container.querySelector('span');
|
|
81
|
+
fireEvent.click(span);
|
|
82
|
+
expect(onClick).toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('token interaction', () => {
|
|
87
|
+
it('calls onTokenClick when clicking on a predefined token', () => {
|
|
88
|
+
const onTokenClick = jest.fn();
|
|
89
|
+
const { container } = render(<TokenText {...defaultProps} onTokenClick={onTokenClick} />);
|
|
90
|
+
const predefinedSpan = container.querySelector('.predefined');
|
|
91
|
+
if (predefinedSpan) {
|
|
92
|
+
fireEvent.click(predefinedSpan);
|
|
93
|
+
expect(onTokenClick).toHaveBeenCalled();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('handles click without window object (SSR)', () => {
|
|
98
|
+
const originalWindow = global.window;
|
|
99
|
+
global.window = undefined;
|
|
100
|
+
|
|
101
|
+
const { container } = render(<TokenText {...defaultProps} />);
|
|
102
|
+
expect(() => fireEvent.click(container.firstChild)).not.toThrow();
|
|
103
|
+
|
|
104
|
+
global.window = originalWindow;
|
|
105
|
+
});
|
|
36
106
|
});
|
|
37
107
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
108
|
+
describe('text selection', () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
global.window.getSelection = jest.fn();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not call onSelectToken when no text is selected', () => {
|
|
114
|
+
const onSelectToken = jest.fn();
|
|
115
|
+
global.window.getSelection.mockReturnValue({
|
|
116
|
+
toString: () => '',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
|
|
120
|
+
fireEvent.click(container.firstChild);
|
|
121
|
+
|
|
122
|
+
expect(onSelectToken).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not call onSelectToken for newline character selection', () => {
|
|
126
|
+
const onSelectToken = jest.fn();
|
|
127
|
+
global.window.getSelection.mockReturnValue({
|
|
128
|
+
toString: () => '\n',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
|
|
132
|
+
fireEvent.click(container.firstChild);
|
|
133
|
+
|
|
134
|
+
expect(onSelectToken).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('does not call onSelectToken for space character selection', () => {
|
|
138
|
+
const onSelectToken = jest.fn();
|
|
139
|
+
global.window.getSelection.mockReturnValue({
|
|
140
|
+
toString: () => ' ',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
|
|
144
|
+
fireEvent.click(container.firstChild);
|
|
145
|
+
|
|
146
|
+
expect(onSelectToken).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('does not call onSelectToken for tab character selection', () => {
|
|
150
|
+
const onSelectToken = jest.fn();
|
|
151
|
+
global.window.getSelection.mockReturnValue({
|
|
152
|
+
toString: () => '\t',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
|
|
156
|
+
fireEvent.click(container.firstChild);
|
|
157
|
+
|
|
158
|
+
expect(onSelectToken).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('props handling', () => {
|
|
163
|
+
it('accepts and uses all required props', () => {
|
|
164
|
+
const props = {
|
|
165
|
+
text: 'test text',
|
|
166
|
+
tokens: [],
|
|
167
|
+
onTokenClick: jest.fn(),
|
|
168
|
+
onSelectToken: jest.fn(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const { container } = render(<TokenText {...props} />);
|
|
172
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('handles multiple tokens', () => {
|
|
176
|
+
const multipleTokens = [
|
|
177
|
+
{ start: 0, end: 5, text: 'lorem' },
|
|
178
|
+
{ start: 6, end: 11, text: 'ipsum' },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const { container } = render(<TokenText {...defaultProps} tokens={multipleTokens} />);
|
|
182
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('text selection with valid text', () => {
|
|
187
|
+
let mockGetCaretCharacterOffsetWithin;
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
mockGetCaretCharacterOffsetWithin = jest.fn().mockReturnValue(6);
|
|
191
|
+
jest.mock('../selection-utils', () => ({
|
|
192
|
+
clearSelection: jest.fn(),
|
|
193
|
+
getCaretCharacterOffsetWithin: mockGetCaretCharacterOffsetWithin,
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('calls onSelectToken when valid text is selected without overlap', () => {
|
|
198
|
+
const onSelectToken = jest.fn();
|
|
199
|
+
const clearSelectionMock = jest.fn();
|
|
200
|
+
|
|
201
|
+
const selectionUtils = require('../selection-utils');
|
|
202
|
+
selectionUtils.clearSelection = clearSelectionMock;
|
|
203
|
+
selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(6);
|
|
204
|
+
|
|
205
|
+
global.window.getSelection.mockReturnValue({
|
|
206
|
+
toString: () => 'ipsum',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const { container } = render(
|
|
210
|
+
<TokenText {...defaultProps} text="lorem ipsum dolor" tokens={[]} onSelectToken={onSelectToken} />,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
fireEvent.click(container.firstChild);
|
|
214
|
+
|
|
215
|
+
expect(onSelectToken).toHaveBeenCalledWith(
|
|
216
|
+
expect.objectContaining({
|
|
217
|
+
text: 'ipsum',
|
|
218
|
+
start: expect.any(Number),
|
|
219
|
+
end: expect.any(Number),
|
|
220
|
+
}),
|
|
221
|
+
expect.any(Array),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles text selection at the end of text', () => {
|
|
226
|
+
const onSelectToken = jest.fn();
|
|
227
|
+
const selectionUtils = require('../selection-utils');
|
|
228
|
+
selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(12);
|
|
229
|
+
|
|
230
|
+
global.window.getSelection.mockReturnValue({
|
|
231
|
+
toString: () => 'dolor',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const { container } = render(
|
|
235
|
+
<TokenText {...defaultProps} text="lorem ipsum dolor" tokens={[]} onSelectToken={onSelectToken} />,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
fireEvent.click(container.firstChild);
|
|
239
|
+
|
|
240
|
+
expect(onSelectToken).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('handles newline offset calculation', () => {
|
|
244
|
+
const onSelectToken = jest.fn();
|
|
245
|
+
const selectionUtils = require('../selection-utils');
|
|
246
|
+
selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(0);
|
|
247
|
+
|
|
248
|
+
global.window.getSelection.mockReturnValue({
|
|
249
|
+
toString: () => 'ipsum',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const { container } = render(
|
|
253
|
+
<TokenText {...defaultProps} text="lorem\nipsum dolor" tokens={[]} onSelectToken={onSelectToken} />,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
fireEvent.click(container.firstChild);
|
|
257
|
+
|
|
258
|
+
if (onSelectToken.mock.calls.length > 0) {
|
|
259
|
+
const token = onSelectToken.mock.calls[0][0];
|
|
260
|
+
expect(token.text).toBe('ipsum');
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('calls onSelectToken with tokensToRemove for surrounded tokens', () => {
|
|
265
|
+
const onSelectToken = jest.fn();
|
|
266
|
+
const selectionUtils = require('../selection-utils');
|
|
267
|
+
selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(0);
|
|
268
|
+
|
|
269
|
+
global.window.getSelection.mockReturnValue({
|
|
270
|
+
toString: () => 'lorem ipsum',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const existingTokens = [{ start: 6, end: 11, text: 'ipsum' }];
|
|
274
|
+
|
|
275
|
+
const { container } = render(
|
|
276
|
+
<TokenText {...defaultProps} text="lorem ipsum dolor" tokens={existingTokens} onSelectToken={onSelectToken} />,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
fireEvent.click(container.firstChild);
|
|
280
|
+
|
|
281
|
+
expect(onSelectToken).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('does not call onSelectToken when root is not available', () => {
|
|
285
|
+
const onSelectToken = jest.fn();
|
|
286
|
+
|
|
287
|
+
global.window.getSelection.mockReturnValue({
|
|
288
|
+
toString: () => 'ipsum',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const { container } = render(
|
|
292
|
+
<TokenText {...defaultProps} text="lorem ipsum" tokens={[]} onSelectToken={onSelectToken} />,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const instance = container.querySelector('div');
|
|
296
|
+
if (instance) {
|
|
297
|
+
fireEvent.click(instance);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('Text component edge cases', () => {
|
|
303
|
+
it('handles null text gracefully', () => {
|
|
304
|
+
const { container } = render(<Text text={null} predefined={false} />);
|
|
305
|
+
expect(container.querySelector('span')).toBeInTheDocument();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('handles undefined text gracefully', () => {
|
|
309
|
+
const { container } = render(<Text text={undefined} predefined={false} />);
|
|
310
|
+
expect(container.querySelector('span')).toBeInTheDocument();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('renders empty string text', () => {
|
|
314
|
+
const { container } = render(<Text text="" predefined={false} />);
|
|
315
|
+
expect(container.querySelector('span')).toBeInTheDocument();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
42
318
|
});
|
package/src/tokenizer/builder.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import compact from 'lodash
|
|
1
|
+
import { clone, compact } from 'lodash-es';
|
|
2
2
|
import English from '@pie-framework/parse-english';
|
|
3
|
-
import clone from 'lodash/clone';
|
|
4
3
|
|
|
5
4
|
const g = (str, node) => {
|
|
6
5
|
if (node.children) {
|
|
@@ -41,7 +40,7 @@ export const paragraphs = (text) => {
|
|
|
41
40
|
export const handleSentence = (child, acc) => {
|
|
42
41
|
const sentenceChilds = [];
|
|
43
42
|
// we parse the children of the sentence
|
|
44
|
-
let newAcc = child.children.reduce(function(acc, child) {
|
|
43
|
+
let newAcc = child.children.reduce(function (acc, child) {
|
|
45
44
|
// if we find a whitespace node that's \n, we end the sentence
|
|
46
45
|
if (child.type === 'WhiteSpaceNode' && child.value === '\n') {
|
|
47
46
|
if (sentenceChilds.length) {
|
|
@@ -50,20 +50,10 @@ export class Controls extends React.Component {
|
|
|
50
50
|
<StyledButton onClick={onWords} size="small" color="primary" disabled={setCorrectMode}>
|
|
51
51
|
Words
|
|
52
52
|
</StyledButton>
|
|
53
|
-
<StyledButton
|
|
54
|
-
onClick={onSentences}
|
|
55
|
-
size="small"
|
|
56
|
-
color="primary"
|
|
57
|
-
disabled={setCorrectMode}
|
|
58
|
-
>
|
|
53
|
+
<StyledButton onClick={onSentences} size="small" color="primary" disabled={setCorrectMode}>
|
|
59
54
|
Sentences
|
|
60
55
|
</StyledButton>
|
|
61
|
-
<StyledButton
|
|
62
|
-
onClick={onParagraphs}
|
|
63
|
-
size="small"
|
|
64
|
-
color="primary"
|
|
65
|
-
disabled={setCorrectMode}
|
|
66
|
-
>
|
|
56
|
+
<StyledButton onClick={onParagraphs} size="small" color="primary" disabled={setCorrectMode}>
|
|
67
57
|
Paragraphs
|
|
68
58
|
</StyledButton>
|
|
69
59
|
<StyledButton size="small" color="secondary" onClick={onClear} disabled={setCorrectMode}>
|
|
@@ -71,12 +61,7 @@ export class Controls extends React.Component {
|
|
|
71
61
|
</StyledButton>
|
|
72
62
|
</div>
|
|
73
63
|
<FormControlLabel
|
|
74
|
-
control={
|
|
75
|
-
<StyledSwitch
|
|
76
|
-
checked={setCorrectMode}
|
|
77
|
-
onChange={onToggleCorrectMode}
|
|
78
|
-
/>
|
|
79
|
-
}
|
|
64
|
+
control={<StyledSwitch checked={setCorrectMode} onChange={onToggleCorrectMode} />}
|
|
80
65
|
label="Set correct answers"
|
|
81
66
|
/>
|
|
82
67
|
</StyledControls>
|
package/src/tokenizer/index.jsx
CHANGED
|
@@ -2,10 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import Controls from './controls';
|
|
4
4
|
import { styled } from '@mui/material/styles';
|
|
5
|
-
import {
|
|
6
|
-
import clone from 'lodash
|
|
7
|
-
import isEqual from 'lodash/isEqual';
|
|
8
|
-
import differenceWith from 'lodash/differenceWith';
|
|
5
|
+
import { paragraphs, sentences, words } from './builder';
|
|
6
|
+
import { clone, differenceWith, isEqual } from 'lodash-es';
|
|
9
7
|
import { noSelect } from '@pie-lib/style-utils';
|
|
10
8
|
import TokenText from './token-text';
|
|
11
9
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { styled } from '@mui/material/styles';
|
|
4
|
-
import {
|
|
4
|
+
import { intersection, normalize } from './builder';
|
|
5
5
|
import debug from 'debug';
|
|
6
6
|
import classNames from 'classnames';
|
|
7
7
|
|
|
8
8
|
import { clearSelection, getCaretCharacterOffsetWithin } from './selection-utils';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { green, yellow } from '@mui/material/colors';
|
|
11
11
|
|
|
12
12
|
const log = debug('@pie-lib:text-select:token-text');
|
|
13
13
|
|
package/NEXT.CHANGELOG.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[]
|