@pie-lib/text-select 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/legend.d.ts +13 -0
  5. package/dist/legend.d.ts.map +1 -0
  6. package/dist/legend.js +64 -0
  7. package/dist/text-select.d.ts +35 -0
  8. package/dist/text-select.d.ts.map +1 -0
  9. package/dist/text-select.js +53 -0
  10. package/dist/token-select/index.d.ts +39 -0
  11. package/dist/token-select/index.d.ts.map +1 -0
  12. package/dist/token-select/index.js +102 -0
  13. package/dist/token-select/token.d.ts +33 -0
  14. package/dist/token-select/token.d.ts.map +1 -0
  15. package/dist/token-select/token.js +134 -0
  16. package/dist/tokenizer/builder.d.ts +28 -0
  17. package/dist/tokenizer/builder.d.ts.map +1 -0
  18. package/dist/tokenizer/builder.js +124 -0
  19. package/dist/tokenizer/controls.d.ts +24 -0
  20. package/dist/tokenizer/controls.d.ts.map +1 -0
  21. package/dist/tokenizer/controls.js +68 -0
  22. package/dist/tokenizer/index.d.ts +36 -0
  23. package/dist/tokenizer/index.d.ts.map +1 -0
  24. package/dist/tokenizer/index.js +91 -0
  25. package/dist/tokenizer/selection-utils.d.ts +11 -0
  26. package/dist/tokenizer/selection-utils.d.ts.map +1 -0
  27. package/dist/tokenizer/selection-utils.js +18 -0
  28. package/dist/tokenizer/token-text.d.ts +28 -0
  29. package/dist/tokenizer/token-text.d.ts.map +1 -0
  30. package/dist/tokenizer/token-text.js +85 -0
  31. package/dist/utils.d.ts +13 -0
  32. package/dist/utils.d.ts.map +1 -0
  33. package/dist/utils.js +21 -0
  34. package/package.json +39 -0
  35. package/src/index.ts +18 -0
  36. package/src/legend.tsx +112 -0
  37. package/src/text-select.tsx +89 -0
  38. package/src/token-select/index.tsx +181 -0
  39. package/src/token-select/token.tsx +233 -0
  40. package/src/tokenizer/builder.ts +268 -0
  41. package/src/tokenizer/controls.tsx +81 -0
  42. package/src/tokenizer/index.tsx +154 -0
  43. package/src/tokenizer/selection-utils.ts +59 -0
  44. package/src/tokenizer/token-text.tsx +145 -0
  45. package/src/utils.tsx +66 -0
@@ -0,0 +1,89 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/text-select.jsx
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import React from 'react';
12
+ import PropTypes from 'prop-types';
13
+
14
+ import TokenSelect from './token-select/index.js';
15
+ import { normalize } from './tokenizer/builder.js';
16
+ import { TokenTypes } from './token-select/token.js';
17
+ import debug from 'debug';
18
+
19
+ const log = debug('@pie-lib:text-select');
20
+ /**
21
+ * Built on TokenSelect uses build.normalize to build the token set.
22
+ */
23
+ export default class TextSelect extends React.Component {
24
+ static propTypes = {
25
+ onChange: PropTypes.func,
26
+ disabled: PropTypes.bool,
27
+ tokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
28
+ selectedTokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
29
+ text: PropTypes.string.isRequired,
30
+ className: PropTypes.string,
31
+ highlightChoices: PropTypes.bool,
32
+ animationsDisabled: PropTypes.bool,
33
+ maxNoOfSelections: PropTypes.number,
34
+ };
35
+
36
+ change: any = (tokens) => {
37
+ const { onChange } = this.props;
38
+
39
+ if (!onChange) {
40
+ return;
41
+ }
42
+ const out = tokens.filter((t) => t.selected).map((t) => ({ start: t.start, end: t.end }));
43
+
44
+ onChange(out);
45
+ };
46
+
47
+ render() {
48
+ const {
49
+ text,
50
+ disabled,
51
+ tokens,
52
+ selectedTokens,
53
+ className,
54
+ highlightChoices,
55
+ maxNoOfSelections,
56
+ animationsDisabled,
57
+ } = this.props;
58
+
59
+ const normalized = normalize(text, tokens);
60
+ log('normalized: ', normalized);
61
+ const prepped = normalized.map((t) => {
62
+ const selectedIndex = selectedTokens.findIndex((s) => {
63
+ return s.start === t.start && s.end === t.end;
64
+ });
65
+ const selected = selectedIndex !== -1;
66
+ const correct = selected ? t.correct : undefined;
67
+ const isMissing = t.isMissing;
68
+ return {
69
+ ...t,
70
+ selectable: !disabled && t.predefined,
71
+ selected,
72
+ correct,
73
+ isMissing,
74
+ };
75
+ });
76
+
77
+ return (
78
+ <TokenSelect
79
+ highlightChoices={!disabled && highlightChoices}
80
+ className={className}
81
+ tokens={prepped}
82
+ disabled={disabled}
83
+ onChange={this.change}
84
+ maxNoOfSelections={maxNoOfSelections}
85
+ animationsDisabled={animationsDisabled}
86
+ />
87
+ );
88
+ }
89
+ }
@@ -0,0 +1,181 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/token-select/index.jsx
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import React from 'react';
12
+ import PropTypes from 'prop-types';
13
+ import Token, { TokenTypes } from './token.js';
14
+ import { styled } from '@mui/material/styles';
15
+ import { clone, isEqual } from 'lodash-es';
16
+ import debug from 'debug';
17
+ import { noSelect } from '@pie-lib/style-utils';
18
+
19
+ const log = debug('@pie-lib:text-select:token-select');
20
+
21
+ const StyledTokenSelect: any = styled('div')(() => ({
22
+ backgroundColor: 'none',
23
+ whiteSpace: 'pre',
24
+ ...noSelect(),
25
+ '& p': {
26
+ whiteSpace: 'break-spaces',
27
+ margin: 0,
28
+ },
29
+ }));
30
+
31
+ // strip HTML tags for plain text rendering
32
+ const stripHtmlTags = (text) => {
33
+ if (!text) {
34
+ return text;
35
+ }
36
+
37
+ return text.replace(/<[^>]+>/g, '');
38
+ };
39
+
40
+ export class TokenSelect extends React.Component {
41
+ static propTypes = {
42
+ tokens: PropTypes.arrayOf(PropTypes.shape(TokenTypes)).isRequired,
43
+ className: PropTypes.string,
44
+ onChange: PropTypes.func.isRequired,
45
+ disabled: PropTypes.bool,
46
+ highlightChoices: PropTypes.bool,
47
+ animationsDisabled: PropTypes.bool,
48
+ maxNoOfSelections: PropTypes.number,
49
+ };
50
+
51
+ static defaultProps = {
52
+ highlightChoices: false,
53
+ maxNoOfSelections: 0,
54
+ tokens: [],
55
+ };
56
+
57
+ selectedCount = () => this.props.tokens.filter((t) => t.selected).length;
58
+
59
+ canSelectMore: any = (selectedCount) => {
60
+ const { maxNoOfSelections } = this.props;
61
+
62
+ if (maxNoOfSelections === 1) return true;
63
+
64
+ log('[canSelectMore] maxNoOfSelections: ', maxNoOfSelections, 'selectedCount: ', selectedCount);
65
+ return maxNoOfSelections <= 0 || (isFinite(maxNoOfSelections) && selectedCount < maxNoOfSelections);
66
+ };
67
+
68
+ toggleToken: any = (event) => {
69
+ const { target } = event;
70
+ const { tokens, animationsDisabled } = this.props;
71
+ const tokensCloned = clone(tokens);
72
+
73
+ const targetSpanWrapper = target.closest?.(`.${Token.rootClassName}`);
74
+ const targetedTokenIndex = targetSpanWrapper?.dataset?.indexkey;
75
+ const t = targetedTokenIndex !== undefined ? tokensCloned[targetedTokenIndex] : undefined;
76
+
77
+ // don't toggle if in print mode, correctness is defined, or is missing
78
+ if (t && t.correct === undefined && !animationsDisabled && !t.isMissing) {
79
+ const { onChange, maxNoOfSelections } = this.props;
80
+ const selected = !t.selected;
81
+
82
+ if (maxNoOfSelections === 1 && this.selectedCount() === 1) {
83
+ const selectedToken = (tokens || []).filter((tk) => tk.selected);
84
+ const updatedTokens = tokensCloned.map((token) => {
85
+ if (isEqual(token, selectedToken[0])) {
86
+ return { ...token, selected: false };
87
+ }
88
+ return { ...token, selectable: true };
89
+ });
90
+
91
+ const update = { ...t, selected };
92
+ updatedTokens.splice(targetedTokenIndex, 1, update);
93
+ onChange(updatedTokens);
94
+ } else {
95
+ if (selected && maxNoOfSelections > 0 && this.selectedCount() >= maxNoOfSelections) {
96
+ log('skip toggle max reached');
97
+ return;
98
+ }
99
+ const update = { ...t, selected };
100
+ tokensCloned.splice(targetedTokenIndex, 1, update);
101
+ onChange(tokensCloned);
102
+ }
103
+ }
104
+ };
105
+
106
+ /** Build a React tree instead of an HTML string so Emotion can inject CSS */
107
+ generateTokensNodes: any = () => {
108
+ const { tokens, disabled, highlightChoices, animationsDisabled } = this.props;
109
+ const selectedCount = this.selectedCount();
110
+
111
+ const isLineBreak = (text) => text === '\n';
112
+ const isNewParagraph = (text) => text === '\n\n';
113
+
114
+ const paragraphs = [];
115
+ let currentChildren = [];
116
+
117
+ const flushParagraph = () => {
118
+ // Always push a <p>, even if empty, to mirror previous behavior
119
+ paragraphs.push(<p key={`p-${paragraphs.length}`}>{currentChildren}</p>);
120
+ currentChildren = [];
121
+ };
122
+
123
+ (tokens || []).forEach((t, index) => {
124
+ const selectable = t.selected || (t.selectable && this.canSelectMore(selectedCount));
125
+ const showCorrectAnswer = t.correct !== undefined && (t.selectable || t.selected);
126
+
127
+ if (isNewParagraph(t.text)) {
128
+ flushParagraph();
129
+ return;
130
+ }
131
+
132
+ if (isLineBreak(t.text)) {
133
+ currentChildren.push(<br key={`br-${index}`} />);
134
+ return;
135
+ }
136
+
137
+ if (
138
+ (selectable && !disabled) ||
139
+ showCorrectAnswer ||
140
+ t.selected ||
141
+ t.isMissing ||
142
+ (animationsDisabled && t.predefined) // print mode
143
+ ) {
144
+ currentChildren.push(
145
+ <Token
146
+ key={index}
147
+ disabled={disabled}
148
+ index={index}
149
+ {...t}
150
+ text={stripHtmlTags(t.text)}
151
+ selectable={selectable}
152
+ highlight={highlightChoices}
153
+ animationsDisabled={animationsDisabled}
154
+ />,
155
+ );
156
+ } else {
157
+ // raw text node – React will escape as needed
158
+ currentChildren.push(<React.Fragment key={index}>{stripHtmlTags(t.text)}</React.Fragment>);
159
+ }
160
+ });
161
+
162
+ // flush last paragraph
163
+ flushParagraph();
164
+
165
+ return paragraphs;
166
+ };
167
+
168
+ render() {
169
+ const { className: classNameProp } = this.props;
170
+ const nodes = this.generateTokensNodes();
171
+
172
+ return (
173
+ <StyledTokenSelect className={classNameProp} onClick={this.toggleToken}>
174
+ {nodes}
175
+ </StyledTokenSelect>
176
+ );
177
+ }
178
+ }
179
+
180
+ export default TokenSelect;
181
+ export { TokenTypes };
@@ -0,0 +1,233 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @synced-from pie-lib/packages/text-select/src/token-select/token.jsx
4
+ * @auto-generated
5
+ *
6
+ * This file is automatically synced from pie-elements and converted to TypeScript.
7
+ * Manual edits will be overwritten on next sync.
8
+ * To make changes, edit the upstream JavaScript file and run sync again.
9
+ */
10
+
11
+ import React from 'react';
12
+ import PropTypes from 'prop-types';
13
+ import { styled } from '@mui/material/styles';
14
+ import classNames from 'classnames';
15
+ import Check from '@mui/icons-material/Check';
16
+ import Close from '@mui/icons-material/Close';
17
+
18
+ import { color } from '@pie-lib/render-ui';
19
+
20
+ // we need to use a larger line height for the token to be more readable
21
+ const LINE_HEIGHT_MULTIPLIER = 3.2;
22
+ // we need a bit more space for correctness indicators
23
+ const CORRECTNESS_LINE_HEIGHT_MULTIPLIER = 3.4;
24
+ const CORRECTNESS_PADDING = 2;
25
+
26
+ // Styled components for different token states
27
+ const StyledToken: any = styled('span')(({ theme }) => ({
28
+ cursor: 'pointer',
29
+ textIndent: 0,
30
+ '&.disabled': {
31
+ cursor: 'inherit',
32
+ color: color.disabled(),
33
+ },
34
+ '&.disabledBlack': {
35
+ cursor: 'inherit',
36
+ pointerEvents: 'none',
37
+ },
38
+ '&.disabledAndSelected': {
39
+ backgroundColor: color.blueGrey100(),
40
+ },
41
+ [`@media (min-width: ${theme.breakpoints.values.md}px)`]: {
42
+ '&.selectable:hover': {
43
+ backgroundColor: color.blueGrey300(),
44
+ color: theme.palette.common.black,
45
+ '& > *': {
46
+ backgroundColor: color.blueGrey300(),
47
+ },
48
+ },
49
+ },
50
+ '&.selected': {
51
+ backgroundColor: color.blueGrey100(),
52
+ color: theme.palette.common.black,
53
+ lineHeight: `${parseFloat(theme.spacing(1)) * LINE_HEIGHT_MULTIPLIER}px`,
54
+ border: `solid 2px ${color.blueGrey900()}`,
55
+ borderRadius: '4px',
56
+ '& > *': {
57
+ backgroundColor: color.blueGrey100(),
58
+ },
59
+ },
60
+ '&.highlight': {
61
+ border: `dashed 2px ${color.blueGrey600()}`,
62
+ borderRadius: '4px',
63
+ lineHeight: `${parseFloat(theme.spacing(1)) * LINE_HEIGHT_MULTIPLIER}px`,
64
+ },
65
+ '&.print': {
66
+ border: `dashed 2px ${color.blueGrey600()}`,
67
+ borderRadius: '4px',
68
+ lineHeight: `${parseFloat(theme.spacing(1)) * LINE_HEIGHT_MULTIPLIER}px`,
69
+ color: color.text(),
70
+ },
71
+ '&.custom': {
72
+ display: 'initial',
73
+ },
74
+ }));
75
+
76
+ const StyledCommonTokenStyle: any = styled('span')(({ theme }) => ({
77
+ position: 'relative',
78
+ borderRadius: '4px',
79
+ color: theme.palette.common.black,
80
+ lineHeight: `${parseFloat(theme.spacing(1)) * CORRECTNESS_LINE_HEIGHT_MULTIPLIER + CORRECTNESS_PADDING}px`,
81
+ padding: `${CORRECTNESS_PADDING}px`,
82
+ }));
83
+
84
+ const StyledCorrectContainer: any = styled(StyledCommonTokenStyle)(() => ({
85
+ border: `${color.correctTertiary()} solid 2px`,
86
+ }));
87
+
88
+ const StyledIncorrectContainer: any = styled(StyledCommonTokenStyle)(() => ({
89
+ border: `${color.incorrectWithIcon()} solid 2px`,
90
+ }));
91
+
92
+ const StyledMissingContainer: any = styled(StyledCommonTokenStyle)(() => ({
93
+ border: `${color.incorrectWithIcon()} dashed 2px`,
94
+ }));
95
+
96
+ const baseIconStyles = {
97
+ color: color.white(),
98
+ position: 'absolute',
99
+ top: '-8px',
100
+ left: '-8px',
101
+ borderRadius: '50%',
102
+ fontSize: '12px',
103
+ padding: '2px',
104
+ display: 'inline-block',
105
+ };
106
+
107
+ const StyledCorrectCheckIcon: any = styled(Check)(() => ({
108
+ ...baseIconStyles,
109
+ backgroundColor: color.correctTertiary(),
110
+ }));
111
+
112
+ const StyledIncorrectCloseIcon: any = styled(Close)(() => ({
113
+ ...baseIconStyles,
114
+ backgroundColor: color.incorrectWithIcon(),
115
+ }));
116
+
117
+ const Wrapper = ({ useWrapper, children, Container, Icon }) =>
118
+ useWrapper ? (
119
+ <Container>
120
+ {children}
121
+ {Icon ? <Icon /> : null}
122
+ </Container>
123
+ ) : (
124
+ children
125
+ );
126
+
127
+ Wrapper.propTypes = {
128
+ useWrapper: PropTypes.bool,
129
+ Container: PropTypes.elementType,
130
+ Icon: PropTypes.elementType,
131
+ children: PropTypes.node,
132
+ };
133
+
134
+ export const TokenTypes = {
135
+ text: PropTypes.string,
136
+ selectable: PropTypes.bool,
137
+ };
138
+
139
+ export class Token extends React.Component {
140
+ static rootClassName = 'tokenRootClass';
141
+
142
+ static propTypes = {
143
+ ...TokenTypes,
144
+ text: PropTypes.string.isRequired,
145
+ className: PropTypes.string,
146
+ disabled: PropTypes.bool,
147
+ highlight: PropTypes.bool,
148
+ correct: PropTypes.bool,
149
+ };
150
+
151
+ static defaultProps = {
152
+ selectable: false,
153
+ text: '',
154
+ };
155
+
156
+ getClassAndIconConfig: any = () => {
157
+ const {
158
+ selectable,
159
+ selected,
160
+ className: classNameProp,
161
+ disabled,
162
+ highlight,
163
+ correct,
164
+ animationsDisabled,
165
+ isMissing,
166
+ } = this.props;
167
+ const isTouchEnabled = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
168
+ const baseClassName = Token.rootClassName;
169
+ let Container;
170
+ let Icon;
171
+
172
+ if (correct === undefined && selected && disabled) {
173
+ return {
174
+ className: classNames(baseClassName, 'selected', 'disabledBlack', classNameProp),
175
+ Component: StyledToken,
176
+ };
177
+ }
178
+
179
+ if (correct !== undefined) {
180
+ const isCorrect = correct === true;
181
+ return {
182
+ className: classNames(baseClassName, 'custom', classNameProp),
183
+ Component: StyledToken,
184
+ Container: isCorrect ? StyledCorrectContainer : StyledIncorrectContainer,
185
+ Icon: isCorrect ? StyledCorrectCheckIcon : StyledIncorrectCloseIcon,
186
+ };
187
+ }
188
+
189
+ if (isMissing) {
190
+ return {
191
+ className: classNames(baseClassName, 'custom', 'missing', classNameProp),
192
+ Component: StyledToken,
193
+ Container: StyledMissingContainer,
194
+ Icon: StyledIncorrectCloseIcon,
195
+ };
196
+ }
197
+
198
+ return {
199
+ className: classNames(
200
+ baseClassName,
201
+ disabled && 'disabled',
202
+ selectable && !disabled && !isTouchEnabled && 'selectable',
203
+ selected && !disabled && 'selected',
204
+ selected && disabled && 'disabledAndSelected',
205
+ highlight && selectable && !disabled && !selected && 'highlight',
206
+ animationsDisabled && 'print',
207
+ classNameProp,
208
+ ),
209
+ Component: StyledToken,
210
+ Container,
211
+ Icon,
212
+ };
213
+ };
214
+
215
+ render() {
216
+ const { text, index, correct, isMissing } = this.props;
217
+ const { className, Component, Container, Icon } = this.getClassAndIconConfig();
218
+
219
+ const TokenComponent = Component || StyledToken;
220
+
221
+ return (
222
+ <Wrapper useWrapper={correct !== undefined || isMissing} Container={Container} Icon={Icon}>
223
+ <TokenComponent
224
+ className={className}
225
+ dangerouslySetInnerHTML={{ __html: (text || '').replace(/\n/g, '<br>') }}
226
+ data-indexkey={index}
227
+ />
228
+ </Wrapper>
229
+ );
230
+ }
231
+ }
232
+
233
+ export default Token;