@pie-lib/text-select 3.0.3-next.1 → 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.
Files changed (70) hide show
  1. package/dist/index.d.ts +15 -0
  2. package/dist/index.js +7 -0
  3. package/dist/legend.d.ts +13 -0
  4. package/dist/legend.js +64 -0
  5. package/dist/node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.js +16 -0
  6. package/dist/text-select.d.ts +34 -0
  7. package/dist/text-select.js +53 -0
  8. package/dist/token-select/index.d.ts +44 -0
  9. package/dist/token-select/index.js +170 -0
  10. package/dist/token-select/token.d.ts +32 -0
  11. package/dist/token-select/token.js +134 -0
  12. package/dist/tokenizer/builder.d.ts +27 -0
  13. package/dist/tokenizer/builder.js +124 -0
  14. package/dist/tokenizer/controls.d.ts +23 -0
  15. package/dist/tokenizer/controls.js +68 -0
  16. package/dist/tokenizer/index.d.ts +35 -0
  17. package/dist/tokenizer/index.js +91 -0
  18. package/dist/tokenizer/selection-utils.d.ts +10 -0
  19. package/dist/tokenizer/selection-utils.js +18 -0
  20. package/dist/tokenizer/token-text.d.ts +27 -0
  21. package/dist/tokenizer/token-text.js +85 -0
  22. package/dist/utils.d.ts +12 -0
  23. package/dist/utils.js +21 -0
  24. package/package.json +33 -30
  25. package/CHANGELOG.json +0 -1
  26. package/CHANGELOG.md +0 -946
  27. package/LICENSE.md +0 -5
  28. package/lib/index.js +0 -57
  29. package/lib/index.js.map +0 -1
  30. package/lib/legend.js +0 -119
  31. package/lib/legend.js.map +0 -1
  32. package/lib/text-select.js +0 -105
  33. package/lib/text-select.js.map +0 -1
  34. package/lib/token-select/index.js +0 -267
  35. package/lib/token-select/index.js.map +0 -1
  36. package/lib/token-select/token.js +0 -236
  37. package/lib/token-select/token.js.map +0 -1
  38. package/lib/tokenizer/builder.js +0 -265
  39. package/lib/tokenizer/builder.js.map +0 -1
  40. package/lib/tokenizer/controls.js +0 -106
  41. package/lib/tokenizer/controls.js.map +0 -1
  42. package/lib/tokenizer/index.js +0 -147
  43. package/lib/tokenizer/index.js.map +0 -1
  44. package/lib/tokenizer/selection-utils.js +0 -55
  45. package/lib/tokenizer/selection-utils.js.map +0 -1
  46. package/lib/tokenizer/token-text.js +0 -176
  47. package/lib/tokenizer/token-text.js.map +0 -1
  48. package/lib/utils.js +0 -51
  49. package/lib/utils.js.map +0 -1
  50. package/src/__tests__/legend.test.jsx +0 -211
  51. package/src/__tests__/text-select.test.jsx +0 -44
  52. package/src/__tests__/utils.test.jsx +0 -27
  53. package/src/index.js +0 -8
  54. package/src/legend.js +0 -102
  55. package/src/text-select.jsx +0 -79
  56. package/src/token-select/__tests__/index.test.jsx +0 -623
  57. package/src/token-select/__tests__/token.test.jsx +0 -236
  58. package/src/token-select/index.jsx +0 -242
  59. package/src/token-select/token.jsx +0 -223
  60. package/src/tokenizer/__tests__/builder.test.js +0 -256
  61. package/src/tokenizer/__tests__/controls.test.jsx +0 -27
  62. package/src/tokenizer/__tests__/index.test.jsx +0 -329
  63. package/src/tokenizer/__tests__/selection-utils.test.js +0 -145
  64. package/src/tokenizer/__tests__/token-text.test.jsx +0 -318
  65. package/src/tokenizer/builder.js +0 -258
  66. package/src/tokenizer/controls.jsx +0 -71
  67. package/src/tokenizer/index.jsx +0 -144
  68. package/src/tokenizer/selection-utils.js +0 -49
  69. package/src/tokenizer/token-text.jsx +0 -135
  70. 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(/&nbsp;/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;