@pie-lib/text-select 3.0.3-next.36 → 3.0.3-next.37
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/dist/index.d.ts +15 -0
- package/dist/index.js +7 -0
- package/dist/legend.d.ts +13 -0
- package/dist/legend.js +64 -0
- package/dist/node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.js +16 -0
- package/dist/text-select.d.ts +34 -0
- package/dist/text-select.js +53 -0
- package/dist/token-select/index.d.ts +44 -0
- package/dist/token-select/index.js +170 -0
- package/dist/token-select/token.d.ts +32 -0
- package/dist/token-select/token.js +134 -0
- package/dist/tokenizer/builder.d.ts +27 -0
- package/dist/tokenizer/builder.js +124 -0
- package/dist/tokenizer/controls.d.ts +23 -0
- package/dist/tokenizer/controls.js +68 -0
- package/dist/tokenizer/index.d.ts +35 -0
- package/dist/tokenizer/index.js +91 -0
- package/dist/tokenizer/selection-utils.d.ts +10 -0
- package/dist/tokenizer/selection-utils.js +18 -0
- package/dist/tokenizer/token-text.d.ts +27 -0
- package/dist/tokenizer/token-text.js +85 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +21 -0
- package/package.json +33 -30
- package/CHANGELOG.json +0 -1
- package/CHANGELOG.md +0 -946
- package/LICENSE.md +0 -5
- package/lib/index.js +0 -57
- package/lib/index.js.map +0 -1
- package/lib/legend.js +0 -119
- package/lib/legend.js.map +0 -1
- package/lib/text-select.js +0 -105
- package/lib/text-select.js.map +0 -1
- package/lib/token-select/index.js +0 -267
- package/lib/token-select/index.js.map +0 -1
- package/lib/token-select/token.js +0 -236
- package/lib/token-select/token.js.map +0 -1
- package/lib/tokenizer/builder.js +0 -265
- package/lib/tokenizer/builder.js.map +0 -1
- package/lib/tokenizer/controls.js +0 -106
- package/lib/tokenizer/controls.js.map +0 -1
- package/lib/tokenizer/index.js +0 -147
- package/lib/tokenizer/index.js.map +0 -1
- package/lib/tokenizer/selection-utils.js +0 -55
- package/lib/tokenizer/selection-utils.js.map +0 -1
- package/lib/tokenizer/token-text.js +0 -176
- package/lib/tokenizer/token-text.js.map +0 -1
- package/lib/utils.js +0 -51
- package/lib/utils.js.map +0 -1
- package/src/__tests__/legend.test.jsx +0 -211
- package/src/__tests__/text-select.test.jsx +0 -44
- package/src/__tests__/utils.test.jsx +0 -27
- package/src/index.js +0 -8
- package/src/legend.js +0 -102
- package/src/text-select.jsx +0 -79
- package/src/token-select/__tests__/index.test.jsx +0 -623
- package/src/token-select/__tests__/token.test.jsx +0 -236
- package/src/token-select/index.jsx +0 -242
- package/src/token-select/token.jsx +0 -223
- package/src/tokenizer/__tests__/builder.test.js +0 -256
- package/src/tokenizer/__tests__/controls.test.jsx +0 -27
- package/src/tokenizer/__tests__/index.test.jsx +0 -329
- package/src/tokenizer/__tests__/selection-utils.test.js +0 -145
- package/src/tokenizer/__tests__/token-text.test.jsx +0 -318
- package/src/tokenizer/builder.js +0 -258
- package/src/tokenizer/controls.jsx +0 -71
- package/src/tokenizer/index.jsx +0 -144
- package/src/tokenizer/selection-utils.js +0 -49
- package/src/tokenizer/token-text.jsx +0 -135
- package/src/utils.js +0 -56
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import { Token } from '../token';
|
|
4
|
-
|
|
5
|
-
describe('token', () => {
|
|
6
|
-
const defaultProps = {
|
|
7
|
-
classes: {
|
|
8
|
-
token: 'token',
|
|
9
|
-
selectable: 'selectable',
|
|
10
|
-
},
|
|
11
|
-
text: 'foo bar',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
jest.clearAllMocks();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('rendering', () => {
|
|
19
|
-
it('renders with text', () => {
|
|
20
|
-
const { container } = render(<Token {...defaultProps} />);
|
|
21
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('renders with newlines', () => {
|
|
25
|
-
const { container } = render(<Token {...defaultProps} text="foo \nbar" />);
|
|
26
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('renders with multiple newlines', () => {
|
|
30
|
-
const { container } = render(<Token {...defaultProps} text="line1\nline2\nline3" />);
|
|
31
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('renders with custom className', () => {
|
|
35
|
-
const { container } = render(<Token {...defaultProps} className="custom-class" />);
|
|
36
|
-
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('renders with data-indexkey attribute', () => {
|
|
40
|
-
const { container } = render(<Token {...defaultProps} index={5} />);
|
|
41
|
-
const token = container.querySelector('[data-indexkey]');
|
|
42
|
-
expect(token).toHaveAttribute('data-indexkey', '5');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('renders empty text gracefully', () => {
|
|
46
|
-
const { container } = render(<Token {...defaultProps} text="" />);
|
|
47
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('renders null text gracefully', () => {
|
|
51
|
-
const { container } = render(<Token {...defaultProps} text={null} />);
|
|
52
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('selectable state', () => {
|
|
57
|
-
it('applies selectable class when selectable is true', () => {
|
|
58
|
-
const { container } = render(<Token {...defaultProps} selectable={true} />);
|
|
59
|
-
expect(container.querySelector('.selectable')).toBeInTheDocument();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('does not apply selectable class when selectable is false', () => {
|
|
63
|
-
const { container } = render(<Token {...defaultProps} selectable={false} />);
|
|
64
|
-
expect(container.querySelector('.selectable')).not.toBeInTheDocument();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('does not apply selectable class when disabled', () => {
|
|
68
|
-
const { container } = render(<Token {...defaultProps} selectable={true} disabled={true} />);
|
|
69
|
-
expect(container.querySelector('.selectable')).not.toBeInTheDocument();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('selected state', () => {
|
|
74
|
-
it('applies selected class when selected is true', () => {
|
|
75
|
-
const { container } = render(<Token {...defaultProps} selected={true} />);
|
|
76
|
-
expect(container.querySelector('.selected')).toBeInTheDocument();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('does not apply selected class when selected is false', () => {
|
|
80
|
-
const { container } = render(<Token {...defaultProps} selected={false} />);
|
|
81
|
-
expect(container.querySelector('.selected')).not.toBeInTheDocument();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('applies disabledBlack class when selected and disabled without correct prop', () => {
|
|
85
|
-
const { container } = render(<Token {...defaultProps} selected={true} disabled={true} />);
|
|
86
|
-
expect(container.querySelector('.disabledBlack')).toBeInTheDocument();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe('disabled state', () => {
|
|
91
|
-
it('applies disabled class when disabled is true', () => {
|
|
92
|
-
const { container } = render(<Token {...defaultProps} disabled={true} />);
|
|
93
|
-
expect(container.querySelector('.disabled')).toBeInTheDocument();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('does not apply disabled class when disabled is false', () => {
|
|
97
|
-
const { container } = render(<Token {...defaultProps} disabled={false} />);
|
|
98
|
-
expect(container.querySelector('.disabled')).not.toBeInTheDocument();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('highlight state', () => {
|
|
103
|
-
it('applies highlight class when highlight is true and selectable', () => {
|
|
104
|
-
const { container } = render(<Token {...defaultProps} highlight={true} selectable={true} />);
|
|
105
|
-
expect(container.querySelector('.highlight')).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('does not apply highlight class when not selectable', () => {
|
|
109
|
-
const { container } = render(<Token {...defaultProps} highlight={true} selectable={false} />);
|
|
110
|
-
expect(container.querySelector('.highlight')).not.toBeInTheDocument();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('does not apply highlight class when disabled', () => {
|
|
114
|
-
const { container } = render(<Token {...defaultProps} highlight={true} selectable={true} disabled={true} />);
|
|
115
|
-
expect(container.querySelector('.highlight')).not.toBeInTheDocument();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('does not apply highlight class when selected', () => {
|
|
119
|
-
const { container } = render(<Token {...defaultProps} highlight={true} selectable={true} selected={true} />);
|
|
120
|
-
expect(container.querySelector('.highlight')).not.toBeInTheDocument();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('correct/incorrect state', () => {
|
|
125
|
-
it('applies custom class when correct is true', () => {
|
|
126
|
-
const { container } = render(<Token {...defaultProps} correct={true} />);
|
|
127
|
-
expect(container.querySelector('.custom')).toBeInTheDocument();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('applies custom class when correct is false', () => {
|
|
131
|
-
const { container } = render(<Token {...defaultProps} correct={false} />);
|
|
132
|
-
expect(container.querySelector('.custom')).toBeInTheDocument();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('renders check icon when correct is true', () => {
|
|
136
|
-
const { container } = render(<Token {...defaultProps} correct={true} />);
|
|
137
|
-
const checkIcon = container.querySelector('svg[data-testid="CheckIcon"]');
|
|
138
|
-
expect(checkIcon).toBeInTheDocument();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('renders close icon when correct is false', () => {
|
|
142
|
-
const { container } = render(<Token {...defaultProps} correct={false} />);
|
|
143
|
-
const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
|
|
144
|
-
expect(closeIcon).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('wraps token in correct container when correct is true', () => {
|
|
148
|
-
const { container } = render(<Token {...defaultProps} correct={true} />);
|
|
149
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('wraps token in incorrect container when correct is false', () => {
|
|
153
|
-
const { container } = render(<Token {...defaultProps} correct={false} />);
|
|
154
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe('missing state', () => {
|
|
159
|
-
it('applies missing class when isMissing is true', () => {
|
|
160
|
-
const { container } = render(<Token {...defaultProps} isMissing={true} />);
|
|
161
|
-
expect(container.querySelector('.missing')).toBeInTheDocument();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('renders close icon when isMissing is true', () => {
|
|
165
|
-
const { container } = render(<Token {...defaultProps} isMissing={true} />);
|
|
166
|
-
const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
|
|
167
|
-
expect(closeIcon).toBeInTheDocument();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('applies custom class when isMissing is true', () => {
|
|
171
|
-
const { container } = render(<Token {...defaultProps} isMissing={true} />);
|
|
172
|
-
expect(container.querySelector('.custom')).toBeInTheDocument();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('animations disabled state', () => {
|
|
177
|
-
it('applies print class when animationsDisabled is true', () => {
|
|
178
|
-
const { container } = render(<Token {...defaultProps} animationsDisabled={true} />);
|
|
179
|
-
expect(container.querySelector('.print')).toBeInTheDocument();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('does not apply print class when animationsDisabled is false', () => {
|
|
183
|
-
const { container } = render(<Token {...defaultProps} animationsDisabled={false} />);
|
|
184
|
-
expect(container.querySelector('.print')).not.toBeInTheDocument();
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe('Wrapper component behavior', () => {
|
|
189
|
-
it('uses wrapper when correct is defined', () => {
|
|
190
|
-
const { container } = render(<Token {...defaultProps} correct={true} />);
|
|
191
|
-
const icon = container.querySelector('svg');
|
|
192
|
-
expect(icon).toBeInTheDocument();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('uses wrapper when isMissing is true', () => {
|
|
196
|
-
const { container } = render(<Token {...defaultProps} isMissing={true} />);
|
|
197
|
-
const icon = container.querySelector('svg');
|
|
198
|
-
expect(icon).toBeInTheDocument();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('does not use wrapper when correct is undefined and isMissing is false', () => {
|
|
202
|
-
const { container } = render(<Token {...defaultProps} />);
|
|
203
|
-
const icon = container.querySelector('svg');
|
|
204
|
-
expect(icon).not.toBeInTheDocument();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('className combinations', () => {
|
|
209
|
-
it('combines multiple state classes', () => {
|
|
210
|
-
const { container } = render(<Token {...defaultProps} selectable={true} selected={true} highlight={true} />);
|
|
211
|
-
expect(container.querySelector('.selected')).toBeInTheDocument();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('applies correct precedence for disabled and selected', () => {
|
|
215
|
-
const { container } = render(<Token {...defaultProps} selectable={true} selected={true} disabled={true} />);
|
|
216
|
-
expect(container.querySelector('.disabledBlack')).toBeInTheDocument();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('applies tokenRootClass to all tokens', () => {
|
|
220
|
-
const { container } = render(<Token {...defaultProps} />);
|
|
221
|
-
expect(container.querySelector('.tokenRootClass')).toBeInTheDocument();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('prop defaults', () => {
|
|
226
|
-
it('uses default value for selectable', () => {
|
|
227
|
-
const { container } = render(<Token text="test" />);
|
|
228
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('uses default value for text', () => {
|
|
232
|
-
const { container } = render(<Token />);
|
|
233
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { renderToString } from 'react-dom/server';
|
|
3
|
-
import PropTypes from 'prop-types';
|
|
4
|
-
import Token, { TokenTypes } from './token';
|
|
5
|
-
import { styled } from '@mui/material/styles';
|
|
6
|
-
import { clone, isEqual } from 'lodash-es';
|
|
7
|
-
import debug from 'debug';
|
|
8
|
-
import { noSelect } from '@pie-lib/style-utils';
|
|
9
|
-
|
|
10
|
-
const log = debug('@pie-lib:text-select:token-select');
|
|
11
|
-
|
|
12
|
-
const StyledTokenSelect = styled('div')(() => ({
|
|
13
|
-
backgroundColor: 'none',
|
|
14
|
-
whiteSpace: 'pre',
|
|
15
|
-
...noSelect(),
|
|
16
|
-
'& p': {
|
|
17
|
-
whiteSpace: 'break-spaces',
|
|
18
|
-
},
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// Invisible container whose only job is to make Emotion inject CSS for all Token variants.
|
|
22
|
-
// renderToString produces correct class names but never triggers Emotion's DOM-side injection,
|
|
23
|
-
// so without this the class names exist in the HTML but have no matching CSS rules.
|
|
24
|
-
const HiddenCssPrimer = styled('div')(() => ({
|
|
25
|
-
display: 'none',
|
|
26
|
-
position: 'absolute',
|
|
27
|
-
visibility: 'hidden',
|
|
28
|
-
pointerEvents: 'none',
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
const normalizeCommonEntities = (text = '') => text.replace(/ /gi, '\u00a0');
|
|
32
|
-
|
|
33
|
-
const normalizeSelectableText = (text = '') =>
|
|
34
|
-
normalizeCommonEntities(text)
|
|
35
|
-
.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
|
|
36
|
-
.replace(/<br\s*\/?>/gi, '\n')
|
|
37
|
-
.replace(/<\/?(table|tbody|tr|td|p)[^>]*>/gi, '');
|
|
38
|
-
|
|
39
|
-
export class TokenSelect extends React.Component {
|
|
40
|
-
static propTypes = {
|
|
41
|
-
tokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
|
|
42
|
-
className: PropTypes.string,
|
|
43
|
-
onChange: PropTypes.func.isRequired,
|
|
44
|
-
disabled: PropTypes.bool,
|
|
45
|
-
highlightChoices: PropTypes.bool,
|
|
46
|
-
animationsDisabled: PropTypes.bool,
|
|
47
|
-
maxNoOfSelections: PropTypes.number,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
static defaultProps = {
|
|
51
|
-
highlightChoices: false,
|
|
52
|
-
maxNoOfSelections: 0,
|
|
53
|
-
tokens: [],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
selectedCount = () => this.props.tokens.filter((t) => t.selected).length;
|
|
57
|
-
|
|
58
|
-
canSelectMore = (selectedCount) => {
|
|
59
|
-
const { maxNoOfSelections } = this.props;
|
|
60
|
-
|
|
61
|
-
if (maxNoOfSelections === 1) return true;
|
|
62
|
-
|
|
63
|
-
log('[canSelectMore] maxNoOfSelections: ', maxNoOfSelections, 'selectedCount: ', selectedCount);
|
|
64
|
-
return maxNoOfSelections <= 0 || (isFinite(maxNoOfSelections) && selectedCount < maxNoOfSelections);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
toggleToken = (event) => {
|
|
68
|
-
const { target } = event;
|
|
69
|
-
const { tokens, animationsDisabled } = this.props;
|
|
70
|
-
const tokensCloned = clone(tokens);
|
|
71
|
-
|
|
72
|
-
const targetSpanWrapper = target.closest?.(`.${Token.rootClassName}`);
|
|
73
|
-
const targetedTokenIndex = targetSpanWrapper?.dataset?.indexkey;
|
|
74
|
-
const t = targetedTokenIndex !== undefined ? tokensCloned[targetedTokenIndex] : undefined;
|
|
75
|
-
|
|
76
|
-
// don't toggle if in print mode, correctness is defined, or is missing
|
|
77
|
-
if (t && t.correct === undefined && !animationsDisabled && !t.isMissing) {
|
|
78
|
-
const { onChange, maxNoOfSelections } = this.props;
|
|
79
|
-
const selected = !t.selected;
|
|
80
|
-
|
|
81
|
-
if (maxNoOfSelections === 1 && this.selectedCount() === 1) {
|
|
82
|
-
const selectedToken = (tokens || []).filter((tk) => tk.selected);
|
|
83
|
-
const updatedTokens = tokensCloned.map((token) => {
|
|
84
|
-
if (isEqual(token, selectedToken[0])) {
|
|
85
|
-
return { ...token, selected: false };
|
|
86
|
-
}
|
|
87
|
-
return { ...token, selectable: true };
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const update = { ...t, selected };
|
|
91
|
-
updatedTokens.splice(targetedTokenIndex, 1, update);
|
|
92
|
-
onChange(updatedTokens);
|
|
93
|
-
} else {
|
|
94
|
-
if (selected && maxNoOfSelections > 0 && this.selectedCount() >= maxNoOfSelections) {
|
|
95
|
-
log('skip toggle max reached');
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const update = { ...t, selected };
|
|
99
|
-
tokensCloned.splice(targetedTokenIndex, 1, update);
|
|
100
|
-
onChange(tokensCloned);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Build an HTML string so that non-selectable token text (which may contain arbitrary or even
|
|
107
|
-
* *partial* HTML — e.g. just an opening <table><tbody><tr><td> in one token and the matching
|
|
108
|
-
* closing tags in another) is preserved exactly as-is. Selectable Token components are
|
|
109
|
-
* serialised via renderToString; their Emotion class names are stable hashes so they match the
|
|
110
|
-
* CSS that the HiddenCssPrimer forces Emotion to inject into the document.
|
|
111
|
-
*/
|
|
112
|
-
generateTokensInHtml = () => {
|
|
113
|
-
const { tokens, disabled, highlightChoices, animationsDisabled } = this.props;
|
|
114
|
-
const selectedCount = this.selectedCount();
|
|
115
|
-
|
|
116
|
-
const reducer = (accumulator, t, index) => {
|
|
117
|
-
const selectable = t.selected || (t.selectable && this.canSelectMore(selectedCount));
|
|
118
|
-
const showCorrectAnswer = t.correct !== undefined && (t.selectable || t.selected);
|
|
119
|
-
|
|
120
|
-
if (t.text === '\n\n') return `${accumulator}</p><p>`;
|
|
121
|
-
|
|
122
|
-
if (t.text === '\n') return `${accumulator}<br>`;
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
(selectable && !disabled) ||
|
|
126
|
-
showCorrectAnswer ||
|
|
127
|
-
t.selected ||
|
|
128
|
-
t.isMissing ||
|
|
129
|
-
(animationsDisabled && t.predefined)
|
|
130
|
-
) {
|
|
131
|
-
return (
|
|
132
|
-
accumulator +
|
|
133
|
-
renderToString(
|
|
134
|
-
<Token
|
|
135
|
-
key={index}
|
|
136
|
-
disabled={disabled}
|
|
137
|
-
index={index}
|
|
138
|
-
{...t}
|
|
139
|
-
text={normalizeSelectableText(t.text)}
|
|
140
|
-
selectable={selectable}
|
|
141
|
-
highlight={highlightChoices}
|
|
142
|
-
animationsDisabled={animationsDisabled}
|
|
143
|
-
/>,
|
|
144
|
-
)
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Non-selectable: emit raw HTML unchanged (may contain partial tags, tables, lists, etc.)
|
|
149
|
-
return accumulator + normalizeCommonEntities(t.text);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
return (tokens || []).reduce(reducer, '<p>') + '</p>';
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
render() {
|
|
156
|
-
const { className: classNameProp } = this.props;
|
|
157
|
-
const html = this.generateTokensInHtml();
|
|
158
|
-
|
|
159
|
-
// Render one invisible Token per visual variant so Emotion injects all CSS rules into the
|
|
160
|
-
// document before the browser paints the dangerouslySetInnerHTML content.
|
|
161
|
-
const primerText = ' ';
|
|
162
|
-
return (
|
|
163
|
-
<>
|
|
164
|
-
<HiddenCssPrimer aria-hidden="true">
|
|
165
|
-
{/* base / selectable */}
|
|
166
|
-
<Token
|
|
167
|
-
text={primerText}
|
|
168
|
-
index={-1}
|
|
169
|
-
selectable
|
|
170
|
-
disabled={false}
|
|
171
|
-
highlight={false}
|
|
172
|
-
animationsDisabled={false}
|
|
173
|
-
/>
|
|
174
|
-
{/* highlight */}
|
|
175
|
-
<Token text={primerText} index={-1} selectable disabled={false} highlight animationsDisabled={false} />
|
|
176
|
-
{/* selected */}
|
|
177
|
-
<Token
|
|
178
|
-
text={primerText}
|
|
179
|
-
index={-1}
|
|
180
|
-
selectable
|
|
181
|
-
selected
|
|
182
|
-
disabled={false}
|
|
183
|
-
highlight={false}
|
|
184
|
-
animationsDisabled={false}
|
|
185
|
-
/>
|
|
186
|
-
{/* disabled + selected */}
|
|
187
|
-
<Token
|
|
188
|
-
text={primerText}
|
|
189
|
-
index={-1}
|
|
190
|
-
selectable
|
|
191
|
-
selected
|
|
192
|
-
disabled
|
|
193
|
-
highlight={false}
|
|
194
|
-
animationsDisabled={false}
|
|
195
|
-
/>
|
|
196
|
-
{/* print / animationsDisabled */}
|
|
197
|
-
<Token
|
|
198
|
-
text={primerText}
|
|
199
|
-
index={-1}
|
|
200
|
-
selectable
|
|
201
|
-
disabled={false}
|
|
202
|
-
highlight={false}
|
|
203
|
-
animationsDisabled
|
|
204
|
-
predefined
|
|
205
|
-
/>
|
|
206
|
-
{/* correct */}
|
|
207
|
-
<Token
|
|
208
|
-
text={primerText}
|
|
209
|
-
index={-1}
|
|
210
|
-
selectable
|
|
211
|
-
selected
|
|
212
|
-
correct
|
|
213
|
-
disabled={false}
|
|
214
|
-
highlight={false}
|
|
215
|
-
animationsDisabled={false}
|
|
216
|
-
/>
|
|
217
|
-
{/* incorrect */}
|
|
218
|
-
<Token
|
|
219
|
-
text={primerText}
|
|
220
|
-
index={-1}
|
|
221
|
-
selectable
|
|
222
|
-
selected
|
|
223
|
-
correct={false}
|
|
224
|
-
disabled={false}
|
|
225
|
-
highlight={false}
|
|
226
|
-
animationsDisabled={false}
|
|
227
|
-
/>
|
|
228
|
-
{/* missing */}
|
|
229
|
-
<Token text={primerText} index={-1} isMissing disabled={false} highlight={false} animationsDisabled={false} />
|
|
230
|
-
</HiddenCssPrimer>
|
|
231
|
-
|
|
232
|
-
<StyledTokenSelect
|
|
233
|
-
className={classNameProp}
|
|
234
|
-
onClick={this.toggleToken}
|
|
235
|
-
dangerouslySetInnerHTML={{ __html: html }}
|
|
236
|
-
/>
|
|
237
|
-
</>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export default TokenSelect;
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
3
|
-
import { styled } from '@mui/material/styles';
|
|
4
|
-
import classNames from 'classnames';
|
|
5
|
-
import Check from '@mui/icons-material/Check';
|
|
6
|
-
import Close from '@mui/icons-material/Close';
|
|
7
|
-
|
|
8
|
-
import { color } from '@pie-lib/render-ui';
|
|
9
|
-
|
|
10
|
-
// we need to use a larger line height for the token to be more readable
|
|
11
|
-
const LINE_HEIGHT_MULTIPLIER = 3.2;
|
|
12
|
-
// we need a bit more space for correctness indicators
|
|
13
|
-
const CORRECTNESS_LINE_HEIGHT_MULTIPLIER = 3.4;
|
|
14
|
-
const CORRECTNESS_PADDING = 2;
|
|
15
|
-
|
|
16
|
-
// Styled components for different token states
|
|
17
|
-
const StyledToken = styled('span')(({ theme }) => ({
|
|
18
|
-
cursor: 'pointer',
|
|
19
|
-
textIndent: 0,
|
|
20
|
-
'&.disabled': {
|
|
21
|
-
cursor: 'inherit',
|
|
22
|
-
color: color.disabled(),
|
|
23
|
-
},
|
|
24
|
-
'&.disabledBlack': {
|
|
25
|
-
cursor: 'inherit',
|
|
26
|
-
pointerEvents: 'none',
|
|
27
|
-
},
|
|
28
|
-
'&.disabledAndSelected': {
|
|
29
|
-
backgroundColor: color.blueGrey100(),
|
|
30
|
-
},
|
|
31
|
-
[`@media (min-width: ${theme.breakpoints.values.md}px)`]: {
|
|
32
|
-
'&.selectable:hover': {
|
|
33
|
-
backgroundColor: color.blueGrey300(),
|
|
34
|
-
color: theme.palette.common.black,
|
|
35
|
-
'& > *': {
|
|
36
|
-
backgroundColor: color.blueGrey300(),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
'&.selected': {
|
|
41
|
-
backgroundColor: color.blueGrey100(),
|
|
42
|
-
color: theme.palette.common.black,
|
|
43
|
-
lineHeight: `${parseFloat(theme.spacing(1)) * LINE_HEIGHT_MULTIPLIER}px`,
|
|
44
|
-
border: `solid 2px ${color.blueGrey900()}`,
|
|
45
|
-
borderRadius: '4px',
|
|
46
|
-
'& > *': {
|
|
47
|
-
backgroundColor: color.blueGrey100(),
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
'&.highlight': {
|
|
51
|
-
border: `dashed 2px ${color.blueGrey600()}`,
|
|
52
|
-
borderRadius: '4px',
|
|
53
|
-
lineHeight: `${parseFloat(theme.spacing(1)) * LINE_HEIGHT_MULTIPLIER}px`,
|
|
54
|
-
},
|
|
55
|
-
'&.print': {
|
|
56
|
-
border: `dashed 2px ${color.blueGrey600()}`,
|
|
57
|
-
borderRadius: '4px',
|
|
58
|
-
lineHeight: `${parseFloat(theme.spacing(1)) * LINE_HEIGHT_MULTIPLIER}px`,
|
|
59
|
-
color: color.text(),
|
|
60
|
-
},
|
|
61
|
-
'&.custom': {
|
|
62
|
-
display: 'initial',
|
|
63
|
-
},
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
const StyledCommonTokenStyle = styled('span')(({ theme }) => ({
|
|
67
|
-
position: 'relative',
|
|
68
|
-
borderRadius: '4px',
|
|
69
|
-
color: theme.palette.common.black,
|
|
70
|
-
lineHeight: `${parseFloat(theme.spacing(1)) * CORRECTNESS_LINE_HEIGHT_MULTIPLIER + CORRECTNESS_PADDING}px`,
|
|
71
|
-
padding: `${CORRECTNESS_PADDING}px`,
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
const StyledCorrectContainer = styled(StyledCommonTokenStyle)(() => ({
|
|
75
|
-
border: `${color.correctTertiary()} solid 2px`,
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
const StyledIncorrectContainer = styled(StyledCommonTokenStyle)(() => ({
|
|
79
|
-
border: `${color.incorrectWithIcon()} solid 2px`,
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
const StyledMissingContainer = styled(StyledCommonTokenStyle)(() => ({
|
|
83
|
-
border: `${color.incorrectWithIcon()} dashed 2px`,
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
const baseIconStyles = {
|
|
87
|
-
color: color.white(),
|
|
88
|
-
position: 'absolute',
|
|
89
|
-
top: '-8px',
|
|
90
|
-
left: '-8px',
|
|
91
|
-
borderRadius: '50%',
|
|
92
|
-
fontSize: '12px',
|
|
93
|
-
padding: '2px',
|
|
94
|
-
display: 'inline-block',
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const StyledCorrectCheckIcon = styled(Check)(() => ({
|
|
98
|
-
...baseIconStyles,
|
|
99
|
-
backgroundColor: color.correctTertiary(),
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
|
-
const StyledIncorrectCloseIcon = styled(Close)(() => ({
|
|
103
|
-
...baseIconStyles,
|
|
104
|
-
backgroundColor: color.incorrectWithIcon(),
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
const Wrapper = ({ useWrapper, children, Container, Icon }) =>
|
|
108
|
-
useWrapper ? (
|
|
109
|
-
<Container>
|
|
110
|
-
{children}
|
|
111
|
-
{Icon ? <Icon /> : null}
|
|
112
|
-
</Container>
|
|
113
|
-
) : (
|
|
114
|
-
children
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
Wrapper.propTypes = {
|
|
118
|
-
useWrapper: PropTypes.bool,
|
|
119
|
-
Container: PropTypes.elementType,
|
|
120
|
-
Icon: PropTypes.elementType,
|
|
121
|
-
children: PropTypes.node,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export const TokenTypes = {
|
|
125
|
-
text: PropTypes.string,
|
|
126
|
-
selectable: PropTypes.bool,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export class Token extends React.Component {
|
|
130
|
-
static rootClassName = 'tokenRootClass';
|
|
131
|
-
|
|
132
|
-
static propTypes = {
|
|
133
|
-
...TokenTypes,
|
|
134
|
-
text: PropTypes.string.isRequired,
|
|
135
|
-
className: PropTypes.string,
|
|
136
|
-
disabled: PropTypes.bool,
|
|
137
|
-
highlight: PropTypes.bool,
|
|
138
|
-
correct: PropTypes.bool,
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
static defaultProps = {
|
|
142
|
-
selectable: false,
|
|
143
|
-
text: '',
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
getClassAndIconConfig = () => {
|
|
147
|
-
const {
|
|
148
|
-
selectable,
|
|
149
|
-
selected,
|
|
150
|
-
className: classNameProp,
|
|
151
|
-
disabled,
|
|
152
|
-
highlight,
|
|
153
|
-
correct,
|
|
154
|
-
animationsDisabled,
|
|
155
|
-
isMissing,
|
|
156
|
-
} = this.props;
|
|
157
|
-
const isTouchEnabled = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
158
|
-
const baseClassName = Token.rootClassName;
|
|
159
|
-
let Container;
|
|
160
|
-
let Icon;
|
|
161
|
-
|
|
162
|
-
if (correct === undefined && selected && disabled) {
|
|
163
|
-
return {
|
|
164
|
-
className: classNames(baseClassName, 'selected', 'disabledBlack', classNameProp),
|
|
165
|
-
Component: StyledToken,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (correct !== undefined) {
|
|
170
|
-
const isCorrect = correct === true;
|
|
171
|
-
return {
|
|
172
|
-
className: classNames(baseClassName, 'custom', classNameProp),
|
|
173
|
-
Component: StyledToken,
|
|
174
|
-
Container: isCorrect ? StyledCorrectContainer : StyledIncorrectContainer,
|
|
175
|
-
Icon: isCorrect ? StyledCorrectCheckIcon : StyledIncorrectCloseIcon,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (isMissing) {
|
|
180
|
-
return {
|
|
181
|
-
className: classNames(baseClassName, 'custom', 'missing', classNameProp),
|
|
182
|
-
Component: StyledToken,
|
|
183
|
-
Container: StyledMissingContainer,
|
|
184
|
-
Icon: StyledIncorrectCloseIcon,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
className: classNames(
|
|
190
|
-
baseClassName,
|
|
191
|
-
disabled && 'disabled',
|
|
192
|
-
selectable && !disabled && !isTouchEnabled && 'selectable',
|
|
193
|
-
selected && !disabled && 'selected',
|
|
194
|
-
selected && disabled && 'disabledAndSelected',
|
|
195
|
-
highlight && selectable && !disabled && !selected && 'highlight',
|
|
196
|
-
animationsDisabled && 'print',
|
|
197
|
-
classNameProp,
|
|
198
|
-
),
|
|
199
|
-
Component: StyledToken,
|
|
200
|
-
Container,
|
|
201
|
-
Icon,
|
|
202
|
-
};
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
render() {
|
|
206
|
-
const { text, index, correct, isMissing } = this.props;
|
|
207
|
-
const { className, Component, Container, Icon } = this.getClassAndIconConfig();
|
|
208
|
-
|
|
209
|
-
const TokenComponent = Component || StyledToken;
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<Wrapper useWrapper={correct !== undefined || isMissing} Container={Container} Icon={Icon}>
|
|
213
|
-
<TokenComponent
|
|
214
|
-
className={className}
|
|
215
|
-
dangerouslySetInnerHTML={{ __html: (text || '').replace(/\n/g, '<br>') }}
|
|
216
|
-
data-indexkey={index}
|
|
217
|
-
/>
|
|
218
|
-
</Wrapper>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export default Token;
|