@khanacademy/perseus 0.4.3 → 1.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.
- package/.eslintrc.js +6 -1
- package/CHANGELOG.md +45 -0
- package/dist/es/index.css +15 -8
- package/dist/es/index.js +1214 -1404
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +15 -8
- package/dist/index.js +1217 -1411
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -4
- package/package.json +9 -8
- package/src/components/number-input.jsx +2 -2
- package/src/components/simple-keypad-input.jsx +18 -21
- package/src/index.js +1 -1
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +426 -135
- package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
- package/src/perseus-api.jsx +10 -36
- package/src/perseus-types.js +18 -145
- package/src/styles/constants.js +0 -2
- package/src/styles/perseus-renderer.less +8 -2
- package/src/styles/variables.less +0 -2
- package/src/styles/widgets/passage.less +9 -5
- package/src/types.js +18 -35
- package/src/util/answer-types.js +2 -3
- package/src/util/math.js +3 -10
- package/src/util.js +0 -6
- package/src/version.js +1 -1
- package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
- package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +291 -130
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3293 -1415
- package/src/widgets/__tests__/group_test.jsx +4 -4
- package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
- package/src/widgets/__tests__/passage_test.jsx +81 -0
- package/src/widgets/__tests__/radio/base-radio_test.jsx +291 -41
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
- package/src/widgets/__tests__/radio/choice_test.jsx +302 -0
- package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
- package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
- package/src/widgets/__tests__/radio_test.jsx +39 -15
- package/src/widgets/definition.jsx +1 -1
- package/src/widgets/group.jsx +84 -102
- package/src/widgets/input-number.jsx +6 -1
- package/src/widgets/numeric-input.jsx +87 -80
- package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
- package/src/widgets/passage/get-line-height-for-node.js +4 -5
- package/src/widgets/passage/passage-markdown.jsx +176 -120
- package/src/widgets/passage.jsx +48 -46
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
- package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
- package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
- package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
- package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
- package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
- package/src/widgets/radio/base-radio.jsx +485 -556
- package/src/widgets/radio/choice-a11y-text.js +62 -0
- package/src/widgets/radio/choice-icon/choice-icon.jsx +87 -0
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +214 -0
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +120 -0
- package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
- package/src/widgets/radio/choice-none-above.jsx +49 -40
- package/src/widgets/radio/choice.jsx +319 -347
- package/src/widgets/radio/choice.jsx.fixture.js +0 -18
- package/src/widgets/radio/focus-ring.jsx +29 -22
- package/src/widgets/radio/option-status.jsx +58 -36
- package/src/widgets/radio/widget.jsx +181 -136
- package/src/widgets/radio.jsx +16 -17
- package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
- package/src/widgets/radio/choice-icon.jsx +0 -469
- package/src/widgets/radio/cross-out-button.jsx +0 -134
package/src/widgets/group.jsx
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
/* eslint-disable react/sort-comp */
|
|
2
1
|
// @flow
|
|
3
2
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
4
3
|
import classNames from "classnames";
|
|
5
4
|
import * as React from "react";
|
|
6
|
-
import _ from "underscore";
|
|
7
5
|
|
|
8
6
|
import * as Changeable from "../mixins/changeable.jsx";
|
|
9
7
|
import {ApiOptions} from "../perseus-api.jsx";
|
|
10
8
|
import Renderer from "../renderer.jsx";
|
|
11
9
|
|
|
12
10
|
import type {PerseusGroupWidgetOptions} from "../perseus-types.js";
|
|
11
|
+
import type {Widget} from "../renderer.jsx";
|
|
13
12
|
import type {
|
|
13
|
+
APIOptions,
|
|
14
|
+
ChangeFn,
|
|
14
15
|
FocusPath,
|
|
15
16
|
PerseusScore,
|
|
16
17
|
WidgetExports,
|
|
@@ -28,6 +29,8 @@ type DefaultProps = {|
|
|
|
28
29
|
|};
|
|
29
30
|
|
|
30
31
|
class Group extends React.Component<Props> {
|
|
32
|
+
rendererRef: ?Renderer;
|
|
33
|
+
|
|
31
34
|
static defaultProps: DefaultProps = {
|
|
32
35
|
content: "",
|
|
33
36
|
widgets: {},
|
|
@@ -42,24 +45,76 @@ class Group extends React.Component<Props> {
|
|
|
42
45
|
this.forceUpdate();
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
change: ChangeFn = (...args) => {
|
|
49
|
+
return Changeable.change.apply(this, args);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
getUserInput: () => $FlowFixMe = () => {
|
|
53
|
+
return this.rendererRef?.getUserInput();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
getSerializedState: () => $FlowFixMe = () => {
|
|
57
|
+
return this.rendererRef?.getSerializedState();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
restoreSerializedState: ($FlowFixMe, $FlowFixMe) => null = (
|
|
61
|
+
state,
|
|
62
|
+
callback,
|
|
63
|
+
) => {
|
|
64
|
+
this.rendererRef?.restoreSerializedState(state, callback);
|
|
65
|
+
// Tell our renderer that we have no props to change
|
|
66
|
+
// (all our changes were in state):
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
simpleValidate: (Rubric) => ?PerseusScore = (rubric) => {
|
|
71
|
+
return this.rendererRef?.score();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Mobile API:
|
|
75
|
+
getInputPaths: () => ?$ReadOnlyArray<FocusPath> = () => {
|
|
76
|
+
return this.rendererRef?.getInputPaths();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
setInputValue: (FocusPath, string, () => mixed) => void = (
|
|
80
|
+
path,
|
|
81
|
+
newValue,
|
|
82
|
+
callback,
|
|
83
|
+
) => {
|
|
84
|
+
return this.rendererRef?.setInputValue(path, newValue, callback);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
focus: () => ?boolean = () => {
|
|
88
|
+
return this.rendererRef?.focus();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
focusInputPath: (FocusPath) => void = (path) => {
|
|
92
|
+
this.rendererRef?.focusPath(path);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
blurInputPath: (FocusPath) => void = (path) => {
|
|
96
|
+
this.rendererRef?.blurPath(path);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
showRationalesForCurrentlySelectedChoices: () => void = () => {
|
|
100
|
+
this.rendererRef?.showRationalesForCurrentlySelectedChoices();
|
|
101
|
+
};
|
|
102
|
+
|
|
45
103
|
render(): React.Node {
|
|
46
|
-
const apiOptions =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.props.onFocus(newFocus);
|
|
59
|
-
}
|
|
60
|
-
},
|
|
104
|
+
const apiOptions: APIOptions = {
|
|
105
|
+
...ApiOptions.defaults,
|
|
106
|
+
...this.props.apiOptions,
|
|
107
|
+
// Api Rewriting to support correct onFocus/onBlur
|
|
108
|
+
// events for the mobile API
|
|
109
|
+
onFocusChange: (newFocus, oldFocus) => {
|
|
110
|
+
if (oldFocus) {
|
|
111
|
+
this.props.onBlur(oldFocus);
|
|
112
|
+
}
|
|
113
|
+
if (newFocus) {
|
|
114
|
+
this.props.onFocus(newFocus);
|
|
115
|
+
}
|
|
61
116
|
},
|
|
62
|
-
|
|
117
|
+
};
|
|
63
118
|
|
|
64
119
|
// Allow a problem number annotation to be added.
|
|
65
120
|
// This is cyclical and should probably be reconsidered. In order to
|
|
@@ -69,7 +124,9 @@ class Group extends React.Component<Props> {
|
|
|
69
124
|
// give ourselves number -1. To combat this, we forceUpdate in
|
|
70
125
|
// componentDidMount so that we can number ourselves properly. But,
|
|
71
126
|
// really we should have a more unidirectional flow. TODO(marcia): fix.
|
|
72
|
-
const
|
|
127
|
+
const groupWidgets: $ReadOnlyArray<Widget> =
|
|
128
|
+
this.props.findWidgets("group");
|
|
129
|
+
const number: number = groupWidgets.indexOf(this);
|
|
73
130
|
const problemNumComponent = this.props.apiOptions.groupAnnotator(
|
|
74
131
|
number,
|
|
75
132
|
this.props.widgetId,
|
|
@@ -81,15 +138,12 @@ class Group extends React.Component<Props> {
|
|
|
81
138
|
// alert our renderer (our parent) of the fact that some interaction
|
|
82
139
|
// has occurred.
|
|
83
140
|
const onInteractWithWidget = (id) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// eslint-disable-next-line react/no-string-refs
|
|
87
|
-
this.change("widgets", this.refs.renderer.props.widgets);
|
|
141
|
+
if (this.rendererRef) {
|
|
142
|
+
this.change("widgets", this.rendererRef.props.widgets);
|
|
88
143
|
}
|
|
89
144
|
};
|
|
90
145
|
|
|
91
|
-
|
|
92
|
-
const score = this.refs.renderer && this.refs.renderer.score();
|
|
146
|
+
const score = this.rendererRef?.score();
|
|
93
147
|
const isValid = score && score.type !== "invalid";
|
|
94
148
|
const isInvalid = score && score.type === "invalid";
|
|
95
149
|
|
|
@@ -104,18 +158,11 @@ class Group extends React.Component<Props> {
|
|
|
104
158
|
})}
|
|
105
159
|
>
|
|
106
160
|
{problemNumComponent}
|
|
107
|
-
{/**
|
|
108
|
-
* We're passing a bunch of extra props to Renderer here that it
|
|
109
|
-
* doesn't need. We should replace {...this.props} with the individual
|
|
110
|
-
* props that are needed.
|
|
111
|
-
* TODO(FEI-4034): Only pass what the Renderer expects.
|
|
112
|
-
*/}
|
|
113
|
-
{/* $FlowFixMe[prop-missing] */}
|
|
114
|
-
{/* $FlowFixMe[incompatible-type] */}
|
|
115
161
|
<Renderer
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
162
|
+
content={this.props.content}
|
|
163
|
+
widgets={this.props.widgets}
|
|
164
|
+
images={this.props.images}
|
|
165
|
+
ref={(ref) => (this.rendererRef = ref)}
|
|
119
166
|
apiOptions={apiOptions}
|
|
120
167
|
findExternalWidgets={this.props.findWidgets}
|
|
121
168
|
reviewMode={!!this.props.reviewModeRubric}
|
|
@@ -129,78 +176,13 @@ class Group extends React.Component<Props> {
|
|
|
129
176
|
</div>
|
|
130
177
|
);
|
|
131
178
|
}
|
|
132
|
-
|
|
133
|
-
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
134
|
-
return Changeable.change.apply(this, args);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
getUserInput: () => $FlowFixMe = () => {
|
|
138
|
-
// eslint-disable-next-line react/no-string-refs
|
|
139
|
-
return this.refs.renderer.getUserInput();
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
getSerializedState: () => $FlowFixMe = () => {
|
|
143
|
-
// eslint-disable-next-line react/no-string-refs
|
|
144
|
-
return this.refs.renderer.getSerializedState();
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
restoreSerializedState: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
|
|
148
|
-
state,
|
|
149
|
-
callback,
|
|
150
|
-
) => {
|
|
151
|
-
// eslint-disable-next-line react/no-string-refs
|
|
152
|
-
this.refs.renderer.restoreSerializedState(state, callback);
|
|
153
|
-
// Tell our renderer that we have no props to change
|
|
154
|
-
// (all our changes were in state):
|
|
155
|
-
return null;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
simpleValidate: (Rubric) => PerseusScore = (rubric) => {
|
|
159
|
-
// eslint-disable-next-line react/no-string-refs
|
|
160
|
-
return this.refs.renderer.score();
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// Mobile API:
|
|
164
|
-
getInputPaths: () => $ReadOnlyArray<FocusPath> = () => {
|
|
165
|
-
// eslint-disable-next-line react/no-string-refs
|
|
166
|
-
return this.refs.renderer.getInputPaths();
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
setInputValue: (FocusPath, string, $FlowFixMe) => void = (
|
|
170
|
-
path,
|
|
171
|
-
newValue,
|
|
172
|
-
cb,
|
|
173
|
-
) => {
|
|
174
|
-
// eslint-disable-next-line react/no-string-refs
|
|
175
|
-
return this.refs.renderer.setInputValue(path, newValue, cb);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
focus: () => ?boolean = () => {
|
|
179
|
-
// eslint-disable-next-line react/no-string-refs
|
|
180
|
-
return this.refs.renderer.focus();
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
focusInputPath: (FocusPath) => void = (path) => {
|
|
184
|
-
// eslint-disable-next-line react/no-string-refs
|
|
185
|
-
this.refs.renderer.focusPath(path);
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
blurInputPath: (FocusPath) => void = (path) => {
|
|
189
|
-
// eslint-disable-next-line react/no-string-refs
|
|
190
|
-
this.refs.renderer.blurPath(path);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
showRationalesForCurrentlySelectedChoices: () => void = () => {
|
|
194
|
-
// eslint-disable-next-line react/no-string-refs
|
|
195
|
-
this.refs.renderer.showRationalesForCurrentlySelectedChoices();
|
|
196
|
-
};
|
|
197
179
|
}
|
|
198
180
|
|
|
199
181
|
const traverseChildWidgets = function (
|
|
200
182
|
props: $FlowFixMe,
|
|
201
183
|
traverseRenderer: $FlowFixMe,
|
|
202
184
|
): $FlowFixMe {
|
|
203
|
-
return
|
|
185
|
+
return {...props, ...traverseRenderer(props)};
|
|
204
186
|
};
|
|
205
187
|
|
|
206
188
|
export default ({
|
|
@@ -332,8 +332,13 @@ class InputNumber extends React.Component<Props> {
|
|
|
332
332
|
if (rubric.answerType == null) {
|
|
333
333
|
rubric.answerType = "number";
|
|
334
334
|
}
|
|
335
|
+
|
|
336
|
+
// note(matthewc): this will get immediately parsed again by
|
|
337
|
+
// `KhanAnswerTypes.number.convertToPredicate`, but a string is
|
|
338
|
+
// expected here
|
|
339
|
+
const stringValue = `${rubric.value}`;
|
|
335
340
|
const val = KhanAnswerTypes.number.createValidatorFunctional(
|
|
336
|
-
|
|
341
|
+
stringValue,
|
|
337
342
|
{
|
|
338
343
|
simplify: rubric.simplify,
|
|
339
344
|
inexact: rubric.inexact || undefined,
|
|
@@ -16,10 +16,17 @@ import KhanAnswerTypes from "../util/answer-types.js";
|
|
|
16
16
|
import KhanMath from "../util/math.js";
|
|
17
17
|
|
|
18
18
|
import type {
|
|
19
|
-
|
|
19
|
+
MathFormat,
|
|
20
20
|
PerseusNumericInputAnswer,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
PerseusNumericInputWidgetOptions,
|
|
22
|
+
PerseusNumericInputAnswerForm,
|
|
23
|
+
} from "../perseus-types";
|
|
24
|
+
import type {
|
|
25
|
+
FocusPath,
|
|
26
|
+
PerseusScore,
|
|
27
|
+
WidgetExports,
|
|
28
|
+
WidgetProps,
|
|
29
|
+
} from "../types";
|
|
23
30
|
|
|
24
31
|
const ParseTex = TexWrangler.parseTex;
|
|
25
32
|
|
|
@@ -36,7 +43,7 @@ const answerFormButtons = [
|
|
|
36
43
|
{title: "Numbers with \u03C0", value: "pi", content: "\u03C0"},
|
|
37
44
|
];
|
|
38
45
|
|
|
39
|
-
const formExamples = {
|
|
46
|
+
const formExamples: {[string]: (PerseusNumericInputAnswerForm) => string} = {
|
|
40
47
|
integer: () => i18n._("an integer, like $6$"),
|
|
41
48
|
proper: (form) =>
|
|
42
49
|
form.simplify === "optional"
|
|
@@ -95,6 +102,8 @@ type State = {|
|
|
|
95
102
|
|};
|
|
96
103
|
|
|
97
104
|
export class NumericInput extends React.Component<Props, State> {
|
|
105
|
+
inputRef: ?(SimpleKeypadInput | InputWithExamples);
|
|
106
|
+
|
|
98
107
|
static defaultProps: DefaultProps = {
|
|
99
108
|
currentValue: "",
|
|
100
109
|
size: "normal",
|
|
@@ -113,14 +122,11 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
113
122
|
};
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
static getOneCorrectAnswerFromRubric(
|
|
117
|
-
rubric
|
|
118
|
-
): ?PerseusNumericInputAnswer {
|
|
119
|
-
const correctAnswers = _.filter(
|
|
120
|
-
rubric.answers,
|
|
125
|
+
static getOneCorrectAnswerFromRubric(rubric: Rubric): ?string {
|
|
126
|
+
const correctAnswers = rubric.answers.filter(
|
|
121
127
|
(answer) => answer.status === "correct",
|
|
122
128
|
);
|
|
123
|
-
const answerStrings =
|
|
129
|
+
const answerStrings = correctAnswers.map((answer) => {
|
|
124
130
|
// Figure out how this answer is supposed to be
|
|
125
131
|
// displayed
|
|
126
132
|
let format = "decimal";
|
|
@@ -147,27 +153,36 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
static validate(useInput: UserInput, rubric: Rubric): PerseusScore {
|
|
150
|
-
const allAnswerForms =
|
|
151
|
-
|
|
152
|
-
const createValidator = (answer) =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
const allAnswerForms = answerFormButtons.map((e) => e["value"]);
|
|
157
|
+
|
|
158
|
+
const createValidator = (answer: PerseusNumericInputAnswer) => {
|
|
159
|
+
const stringAnswer = `${answer.value}`;
|
|
160
|
+
return KhanAnswerTypes.number.createValidatorFunctional(
|
|
161
|
+
stringAnswer,
|
|
162
|
+
{
|
|
163
|
+
message: answer.message,
|
|
164
|
+
simplify:
|
|
165
|
+
answer.status === "correct"
|
|
166
|
+
? answer.simplify
|
|
167
|
+
: "optional",
|
|
168
|
+
inexact: true, // TODO(merlob) backfill / delete
|
|
169
|
+
maxError: answer.maxError,
|
|
170
|
+
forms:
|
|
171
|
+
answer.strict &&
|
|
172
|
+
answer.answerForms &&
|
|
173
|
+
answer.answerForms.length !== 0
|
|
174
|
+
? answer.answerForms
|
|
175
|
+
: allAnswerForms,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
};
|
|
166
179
|
|
|
167
180
|
// We may have received TeX; try to parse it before grading.
|
|
168
181
|
// If `currentValue` is not TeX, this should be a no-op.
|
|
169
182
|
const currentValue = ParseTex(useInput.currentValue);
|
|
170
|
-
const correctAnswers =
|
|
183
|
+
const correctAnswers = rubric.answers.filter(
|
|
184
|
+
(answer) => answer.status === "correct",
|
|
185
|
+
);
|
|
171
186
|
|
|
172
187
|
const normalizedAnswerExpected = correctAnswers.every(
|
|
173
188
|
(answer) => Math.abs(answer.value) <= 1,
|
|
@@ -177,8 +192,8 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
177
192
|
// precisely or approximately and return the appropriate message:
|
|
178
193
|
// - if precise, return the message that the answer came with
|
|
179
194
|
// - if it needs to be simplified, etc., show that message
|
|
180
|
-
let result =
|
|
181
|
-
|
|
195
|
+
let result = correctAnswers
|
|
196
|
+
.map((answer) => {
|
|
182
197
|
// The coefficient is an attribute of the widget
|
|
183
198
|
let localValue = currentValue;
|
|
184
199
|
if (rubric.coefficient) {
|
|
@@ -195,20 +210,19 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
195
210
|
normalizedAnswerExpected,
|
|
196
211
|
),
|
|
197
212
|
);
|
|
198
|
-
})
|
|
199
|
-
(match) => match.correct || match.empty
|
|
200
|
-
);
|
|
213
|
+
})
|
|
214
|
+
.find((match) => match.correct || match.empty);
|
|
201
215
|
|
|
202
216
|
if (!result) {
|
|
203
217
|
// Otherwise, if the guess is not correct
|
|
204
218
|
const otherAnswers = [].concat(
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
rubric.answers.filter((answer) => answer.status === "ungraded"),
|
|
220
|
+
rubric.answers.filter((answer) => answer.status === "wrong"),
|
|
207
221
|
);
|
|
208
222
|
|
|
209
223
|
// Look through all other answers and if one matches either
|
|
210
224
|
// precisely or approximately return the answer's message
|
|
211
|
-
const match =
|
|
225
|
+
const match = otherAnswers.find((answer) => {
|
|
212
226
|
const validate = createValidator(answer);
|
|
213
227
|
return validate(
|
|
214
228
|
maybeParsePercentInput(
|
|
@@ -259,14 +273,13 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
259
273
|
correct = score.type === "points" && score.earned === score.total;
|
|
260
274
|
|
|
261
275
|
if (!correct) {
|
|
262
|
-
const correctAnswers =
|
|
263
|
-
rubric.answers,
|
|
276
|
+
const correctAnswers = rubric.answers.filter(
|
|
264
277
|
(answer) => answer.status === "correct",
|
|
265
278
|
);
|
|
266
|
-
const answerStrings =
|
|
279
|
+
const answerStrings = correctAnswers.map((answer) => {
|
|
267
280
|
// Figure out how this answer is supposed to be
|
|
268
281
|
// displayed
|
|
269
|
-
let format = "decimal";
|
|
282
|
+
let format: MathFormat = "decimal";
|
|
270
283
|
if (answer.answerForms && answer.answerForms[0]) {
|
|
271
284
|
// NOTE(johnsullivan): This isn't exactly ideal, but
|
|
272
285
|
// it does behave well for all the currently known
|
|
@@ -298,7 +311,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
298
311
|
const forms =
|
|
299
312
|
this.props.answerForms?.length !== 0
|
|
300
313
|
? this.props.answerForms
|
|
301
|
-
:
|
|
314
|
+
: Object.keys(formExamples).map((name) => {
|
|
302
315
|
return {
|
|
303
316
|
name: name,
|
|
304
317
|
simplify: "required",
|
|
@@ -320,10 +333,11 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
320
333
|
// To check if all answer forms are accepted, we must first
|
|
321
334
|
// find the *names* of all accepted forms, and see if they are
|
|
322
335
|
// all present, ignoring duplicates
|
|
323
|
-
const answerFormNames = _.uniq(
|
|
336
|
+
const answerFormNames: $ReadOnlyArray<string> = _.uniq(
|
|
324
337
|
this.props.answerForms?.map((form) => form.name),
|
|
325
338
|
);
|
|
326
|
-
const allFormsAccepted =
|
|
339
|
+
const allFormsAccepted =
|
|
340
|
+
answerFormNames.length >= Object.keys(formExamples).length;
|
|
327
341
|
return !noFormsAccepted && !allFormsAccepted;
|
|
328
342
|
};
|
|
329
343
|
|
|
@@ -332,20 +346,16 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
332
346
|
};
|
|
333
347
|
|
|
334
348
|
focus: () => boolean = () => {
|
|
335
|
-
|
|
336
|
-
this.refs.input.focus();
|
|
349
|
+
this.inputRef?.focus();
|
|
337
350
|
return true;
|
|
338
351
|
};
|
|
339
352
|
|
|
340
|
-
focusInputPath: (
|
|
341
|
-
|
|
342
|
-
// eslint-disable-next-line react/no-string-refs
|
|
343
|
-
this.refs.input.focus();
|
|
353
|
+
focusInputPath: () => void = () => {
|
|
354
|
+
this.inputRef?.focus();
|
|
344
355
|
};
|
|
345
356
|
|
|
346
|
-
blurInputPath: (
|
|
347
|
-
|
|
348
|
-
this.refs.input.blur();
|
|
357
|
+
blurInputPath: () => void = () => {
|
|
358
|
+
this.inputRef?.blur();
|
|
349
359
|
};
|
|
350
360
|
|
|
351
361
|
getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<string>> = () => {
|
|
@@ -355,12 +365,12 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
355
365
|
return [[]];
|
|
356
366
|
};
|
|
357
367
|
|
|
358
|
-
getGrammarTypeForPath: (
|
|
368
|
+
getGrammarTypeForPath: (FocusPath) => string = (inputPath) => {
|
|
359
369
|
/* istanbul ignore next */
|
|
360
370
|
return "number";
|
|
361
371
|
};
|
|
362
372
|
|
|
363
|
-
setInputValue: (
|
|
373
|
+
setInputValue: (FocusPath, string, ?() => mixed) => void = (
|
|
364
374
|
path,
|
|
365
375
|
newValue,
|
|
366
376
|
cb,
|
|
@@ -378,7 +388,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
378
388
|
return NumericInput.getUserInputFromProps(this.props);
|
|
379
389
|
};
|
|
380
390
|
|
|
381
|
-
handleChange: (string,
|
|
391
|
+
handleChange: (string, ?() => mixed) => void = (newValue, cb) => {
|
|
382
392
|
this.props.onChange({currentValue: newValue}, cb);
|
|
383
393
|
this.props.trackInteraction();
|
|
384
394
|
};
|
|
@@ -429,8 +439,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
429
439
|
// TODO(charlie): Support "Review Mode".
|
|
430
440
|
return maybeRightAlignKeypadInput(
|
|
431
441
|
<SimpleKeypadInput
|
|
432
|
-
|
|
433
|
-
ref="input"
|
|
442
|
+
ref={(ref) => (this.inputRef = ref)}
|
|
434
443
|
value={this.props.currentValue}
|
|
435
444
|
keypadElement={this.props.keypadElement}
|
|
436
445
|
onChange={this.handleChange}
|
|
@@ -442,8 +451,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
442
451
|
|
|
443
452
|
const input = (
|
|
444
453
|
<InputWithExamples
|
|
445
|
-
|
|
446
|
-
ref="input"
|
|
454
|
+
ref={(ref) => (this.inputRef = ref)}
|
|
447
455
|
value={this.props.currentValue}
|
|
448
456
|
onChange={this.handleChange}
|
|
449
457
|
className={classNames(classes)}
|
|
@@ -493,7 +501,9 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
493
501
|
// a given *problem* rather than for each possible [correct/wrong] *answer*.
|
|
494
502
|
// When should two answers to a problem take different answer types?
|
|
495
503
|
// See D27790 for more discussion.
|
|
496
|
-
export const unionAnswerForms: (
|
|
504
|
+
export const unionAnswerForms: (
|
|
505
|
+
$ReadOnlyArray<$ReadOnlyArray<PerseusNumericInputAnswerForm>>,
|
|
506
|
+
) => $ReadOnlyArray<PerseusNumericInputAnswerForm> = function (
|
|
497
507
|
answerFormsList,
|
|
498
508
|
) {
|
|
499
509
|
// Takes a list of lists of answer forms, and returns a list of the forms
|
|
@@ -504,28 +514,25 @@ export const unionAnswerForms: ($FlowFixMe) => $FlowFixMe = function (
|
|
|
504
514
|
// two elements are equal, and returns a list of unique elements. This is
|
|
505
515
|
// just a helper function here, but works generally.
|
|
506
516
|
const uniqueBy = function (list, iteratee) {
|
|
507
|
-
return
|
|
508
|
-
list
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return uniqueList.concat([element]);
|
|
517
|
-
},
|
|
518
|
-
[],
|
|
519
|
-
);
|
|
517
|
+
return list.reduce((uniqueList, element) => {
|
|
518
|
+
// For each element, decide whether it's already in the list of
|
|
519
|
+
// unique items.
|
|
520
|
+
const inList = _.find(uniqueList, iteratee.bind(null, element));
|
|
521
|
+
if (inList) {
|
|
522
|
+
return uniqueList;
|
|
523
|
+
}
|
|
524
|
+
return uniqueList.concat([element]);
|
|
525
|
+
}, []);
|
|
520
526
|
};
|
|
521
527
|
|
|
522
528
|
// Pull out all of the forms from the different lists.
|
|
523
|
-
const allForms =
|
|
529
|
+
const allForms = answerFormsList.flat();
|
|
524
530
|
// Pull out the unique forms using uniqueBy.
|
|
525
531
|
const uniqueForms = uniqueBy(allForms, _.isEqual);
|
|
526
532
|
// Sort them by the order they appear in the `formExamples` list.
|
|
533
|
+
const formExampleKeys = Object.keys(formExamples);
|
|
527
534
|
return _.sortBy(uniqueForms, (form) => {
|
|
528
|
-
return
|
|
535
|
+
return formExampleKeys.indexOf(form.name);
|
|
529
536
|
});
|
|
530
537
|
};
|
|
531
538
|
|
|
@@ -547,10 +554,10 @@ type RenderProps = {|
|
|
|
547
554
|
// can accept several input forms with or without "%", the decision
|
|
548
555
|
// to parse based on the presence of "%" in the input, is so that we
|
|
549
556
|
// don't accidently scale the user typed value before grading, CP-930.
|
|
550
|
-
export
|
|
551
|
-
inputValue,
|
|
552
|
-
normalizedAnswerExpected,
|
|
553
|
-
)
|
|
557
|
+
export function maybeParsePercentInput(
|
|
558
|
+
inputValue: string | number,
|
|
559
|
+
normalizedAnswerExpected: boolean,
|
|
560
|
+
): string | number {
|
|
554
561
|
// If the input value is not a string ending with "%", then there's
|
|
555
562
|
// nothing more to do. The value will be graded as inputted by user.
|
|
556
563
|
if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
|
|
@@ -575,7 +582,7 @@ export const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
|
|
|
575
582
|
|
|
576
583
|
// Otherwise, we return input valuåe (number) stripped of the "%".
|
|
577
584
|
return value;
|
|
578
|
-
}
|
|
585
|
+
}
|
|
579
586
|
|
|
580
587
|
const propsTransform = function (
|
|
581
588
|
widgetOptions: PerseusNumericInputWidgetOptions,
|
|
@@ -584,7 +591,7 @@ const propsTransform = function (
|
|
|
584
591
|
answerForms: unionAnswerForms(
|
|
585
592
|
// Pull out the name of each form and whether that form has
|
|
586
593
|
// required simplification.
|
|
587
|
-
|
|
594
|
+
widgetOptions.answers.map((answer) => {
|
|
588
595
|
return _.map(answer.answerForms, (form) => {
|
|
589
596
|
return {
|
|
590
597
|
simplify: answer.simplify,
|
|
@@ -7,7 +7,6 @@ import _ from "underscore";
|
|
|
7
7
|
import PassageMarkdown from "../passage-markdown.jsx";
|
|
8
8
|
|
|
9
9
|
const parse = PassageMarkdown.parse;
|
|
10
|
-
const rules = PassageMarkdown._rulesForTesting;
|
|
11
10
|
|
|
12
11
|
const validateParse = (parsed, expected) => {
|
|
13
12
|
if (!_.isEqual(parsed, expected)) {
|
|
@@ -352,14 +351,6 @@ describe.skip("passage markdown", () => {
|
|
|
352
351
|
});
|
|
353
352
|
});
|
|
354
353
|
|
|
355
|
-
describe("orders", () => {
|
|
356
|
-
it("should always be numbers", () => {
|
|
357
|
-
_.each(rules, (rule) => {
|
|
358
|
-
expect(typeof rule.order === "number").toBeTruthy();
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
354
|
describe("output", () => {
|
|
364
355
|
it("should output some basic formatted passage text", () => {
|
|
365
356
|
assertParsesToReact(
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
|
|
3
|
-
type JQueryCollection = $FlowFixMe;
|
|
2
|
+
import $ from "jquery";
|
|
4
3
|
|
|
5
4
|
export const getLineHeightForNode = (
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
line1: HTMLDivElement,
|
|
6
|
+
line2: HTMLDivElement,
|
|
8
7
|
): number => {
|
|
9
|
-
return $line2.offset().top - $line1.offset().top;
|
|
8
|
+
return $(line2).offset().top - $(line1).offset().top;
|
|
10
9
|
};
|