@khanacademy/perseus-editor 0.0.1 → 0.1.2
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/.babelrc.js +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/es/index.css +1020 -0
- package/dist/es/index.js +9 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +1020 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +6 -1
- package/package.json +16 -4
- package/src/README.md +7 -0
- package/src/__stories__/editor.stories.js +33 -0
- package/src/__stories__/section-control-button.stories.jsx +27 -0
- package/src/__testdata__/input-number_testdata.js +28 -0
- package/src/__tests__/editor_test.jsx +123 -0
- package/src/__tests__/i18n_test.jsx +585 -0
- package/src/__tests__/traversal_test.jsx +378 -0
- package/src/all-editors.js +85 -0
- package/src/all-editors_test.js +17 -0
- package/src/all-widgets.js +10 -0
- package/src/article-editor.jsx +437 -0
- package/src/components/__stories__/blur-input.stories.jsx +24 -0
- package/src/components/blur-input.jsx +62 -0
- package/src/components/checkbox.jsx +194 -0
- package/src/components/device-framer.jsx +91 -0
- package/src/components/drag-target.jsx +88 -0
- package/src/components/dropdown-option.jsx +318 -0
- package/src/components/form-wrapped-text-field.jsx +248 -0
- package/src/components/graph-settings.jsx +631 -0
- package/src/components/hover-behavior.jsx +178 -0
- package/src/components/json-editor.jsx +121 -0
- package/src/components/link.jsx +173 -0
- package/src/components/simple-button.jsx +86 -0
- package/src/components/sortable.jsx +186 -0
- package/src/components/util.js +39 -0
- package/src/components/viewport-resizer.jsx +73 -0
- package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
- package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
- package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
- package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
- package/src/diffs/__tests__/split-diff_test.js +34 -0
- package/src/diffs/__tests__/string-array-diff_test.js +74 -0
- package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
- package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
- package/src/diffs/article-diff.jsx +70 -0
- package/src/diffs/item-diff.jsx +77 -0
- package/src/diffs/renderer-diff.jsx +143 -0
- package/src/diffs/split-diff.jsx +30 -0
- package/src/diffs/string-array-diff.jsx +75 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +194 -0
- package/src/diffs/widget-diff-performer.jsx +74 -0
- package/src/diffs/widget-diff.jsx +291 -0
- package/src/editor-page.jsx +351 -0
- package/src/editor.jsx +1364 -0
- package/src/hint-editor.jsx +447 -0
- package/src/i18n.jsx +139 -0
- package/src/iframe-content-renderer.jsx +201 -0
- package/src/index.js +27 -8
- package/src/item-editor.jsx +159 -0
- package/src/item-extras-editor.jsx +150 -0
- package/src/katex-error-view.jsx +87 -0
- package/src/multirenderer-editor.jsx +1074 -0
- package/src/section-control-button.jsx +53 -0
- package/src/stateful-article-editor.jsx +176 -0
- package/src/stateful-editor-page.jsx +90 -0
- package/src/styles/perseus-editor.less +1339 -0
- package/src/testing-widgets.js +12 -0
- package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
- package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
- package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
- package/src/widgets/categorizer-editor.jsx +103 -0
- package/src/widgets/cs-program-editor.jsx +258 -0
- package/src/widgets/definition-editor.jsx +75 -0
- package/src/widgets/dropdown-editor.jsx +201 -0
- package/src/widgets/example-graphie-widget-editor.jsx +70 -0
- package/src/widgets/example-graphie-widget.jsx +175 -0
- package/src/widgets/example-widget-editor.jsx +60 -0
- package/src/widgets/example-widget.jsx +153 -0
- package/src/widgets/explanation-editor.jsx +89 -0
- package/src/widgets/expression-editor.jsx +560 -0
- package/src/widgets/graded-group-editor.jsx +162 -0
- package/src/widgets/graded-group-set-editor.jsx +101 -0
- package/src/widgets/grapher-editor.jsx +154 -0
- package/src/widgets/group-editor.jsx +83 -0
- package/src/widgets/iframe-editor.jsx +200 -0
- package/src/widgets/image-editor.jsx +326 -0
- package/src/widgets/input-number-editor.jsx +269 -0
- package/src/widgets/interaction/constraint-editor.jsx +147 -0
- package/src/widgets/interaction-editor.jsx +1389 -0
- package/src/widgets/interactive-graph-editor.jsx +322 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
- package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
- package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
- package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
- package/src/widgets/label-image/answer-choices.jsx +231 -0
- package/src/widgets/label-image/behavior.jsx +92 -0
- package/src/widgets/label-image/marker.jsx +292 -0
- package/src/widgets/label-image/question-markers.jsx +181 -0
- package/src/widgets/label-image/select-image.jsx +86 -0
- package/src/widgets/label-image-editor.jsx +257 -0
- package/src/widgets/lights-puzzle-editor.jsx +267 -0
- package/src/widgets/matcher-editor.jsx +147 -0
- package/src/widgets/matrix-editor.jsx +155 -0
- package/src/widgets/measurer-editor.jsx +245 -0
- package/src/widgets/molecule-editor.jsx +63 -0
- package/src/widgets/number-line-editor.jsx +498 -0
- package/src/widgets/numeric-input-editor.jsx +554 -0
- package/src/widgets/orderer-editor.jsx +166 -0
- package/src/widgets/passage-editor.jsx +115 -0
- package/src/widgets/passage-ref-editor.jsx +77 -0
- package/src/widgets/passage-ref-target-editor.jsx +47 -0
- package/src/widgets/plotter-editor.jsx +537 -0
- package/src/widgets/radio/editor.jsx +398 -0
- package/src/widgets/reaction-diagram-editor.jsx +127 -0
- package/src/widgets/sequence-editor.jsx +205 -0
- package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
- package/src/widgets/simple-markdown-tester.jsx +91 -0
- package/src/widgets/simulator-editor.jsx +144 -0
- package/src/widgets/sorter-editor.jsx +100 -0
- package/src/widgets/table-editor.jsx +164 -0
- package/src/widgets/transformer-editor.jsx +1006 -0
- package/src/widgets/unit-editor.jsx +293 -0
- package/src/widgets/video-editor.jsx +74 -0
- package/src/__tests__/index.test.js +0 -15
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* eslint-disable react/forbid-prop-types */
|
|
2
|
+
// @flow
|
|
3
|
+
import {components, Changeable, EditorJsonify} from "@khanacademy/perseus";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
import Editor from "../editor.jsx";
|
|
9
|
+
|
|
10
|
+
const {TextInput} = components;
|
|
11
|
+
|
|
12
|
+
const defaultExplanationProps = {
|
|
13
|
+
showPrompt: "Explain",
|
|
14
|
+
hidePrompt: "Hide explanation",
|
|
15
|
+
explanation: "explanation goes here\n\nmore explanation",
|
|
16
|
+
widgets: {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Props = $FlowFixMe;
|
|
20
|
+
type State = $FlowFixMe;
|
|
21
|
+
|
|
22
|
+
class ExplanationEditor extends React.Component<Props, State> {
|
|
23
|
+
static propTypes = {
|
|
24
|
+
...Changeable.propTypes,
|
|
25
|
+
showPrompt: PropTypes.string,
|
|
26
|
+
hidePrompt: PropTypes.string,
|
|
27
|
+
explanation: PropTypes.string,
|
|
28
|
+
widgets: PropTypes.object,
|
|
29
|
+
apiOptions: PropTypes.any,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
static widgetName: "explanation" = "explanation";
|
|
33
|
+
static defaultProps: Props = defaultExplanationProps;
|
|
34
|
+
state: State = {};
|
|
35
|
+
|
|
36
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
37
|
+
return Changeable.change.apply(this, args);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
serialize: () => $FlowFixMe = () => {
|
|
41
|
+
return EditorJsonify.serialize.call(this);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
render(): React.Node {
|
|
45
|
+
return (
|
|
46
|
+
<div className="perseus-widget-explanation-editor">
|
|
47
|
+
<div className="perseus-widget-row">
|
|
48
|
+
<label>
|
|
49
|
+
Prompt to show explanation:{" "}
|
|
50
|
+
<TextInput
|
|
51
|
+
value={this.props.showPrompt}
|
|
52
|
+
onChange={this.change("showPrompt")}
|
|
53
|
+
/>
|
|
54
|
+
</label>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="perseus-widget-row">
|
|
57
|
+
<label>
|
|
58
|
+
Prompt to hide explanation:{" "}
|
|
59
|
+
<TextInput
|
|
60
|
+
value={this.props.hidePrompt}
|
|
61
|
+
onChange={this.change("hidePrompt")}
|
|
62
|
+
/>
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="perseus-widget-row">
|
|
66
|
+
<Editor
|
|
67
|
+
apiOptions={this.props.apiOptions}
|
|
68
|
+
content={this.props.explanation}
|
|
69
|
+
widgets={this.props.widgets}
|
|
70
|
+
widgetEnabled={true}
|
|
71
|
+
immutableWidgets={false}
|
|
72
|
+
onChange={(props) => {
|
|
73
|
+
const newProps = {};
|
|
74
|
+
if (_.has(props, "content")) {
|
|
75
|
+
newProps.explanation = props.content;
|
|
76
|
+
}
|
|
77
|
+
if (_.has(props, "widgets")) {
|
|
78
|
+
newProps.widgets = props.widgets;
|
|
79
|
+
}
|
|
80
|
+
this.change(newProps);
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default ExplanationEditor;
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
/* eslint-disable one-var, react/forbid-prop-types, react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import * as KAS from "@khanacademy/kas";
|
|
4
|
+
import {
|
|
5
|
+
components,
|
|
6
|
+
Changeable,
|
|
7
|
+
Dependencies,
|
|
8
|
+
Expression,
|
|
9
|
+
} from "@khanacademy/perseus";
|
|
10
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
11
|
+
import lens from "hubble";
|
|
12
|
+
import PropTypes from "prop-types";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import _ from "underscore";
|
|
15
|
+
|
|
16
|
+
import SortableArea from "../components/sortable.jsx";
|
|
17
|
+
|
|
18
|
+
const {InfoTip, PropCheckBox, TexButtons} = components;
|
|
19
|
+
const {getDependencies} = Dependencies;
|
|
20
|
+
|
|
21
|
+
// An answer can be considered correct, wrong, or ungraded.
|
|
22
|
+
const CONSIDERED = ["correct", "wrong", "ungraded"];
|
|
23
|
+
|
|
24
|
+
const answerFormType = PropTypes.shape({
|
|
25
|
+
considered: PropTypes.oneOf(CONSIDERED).isRequired,
|
|
26
|
+
value: PropTypes.string.isRequired,
|
|
27
|
+
form: PropTypes.bool.isRequired,
|
|
28
|
+
simplify: PropTypes.bool.isRequired,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Pick a key that isn't currently used by an answer in answerForms
|
|
32
|
+
const _makeNewKey = (answerForms) => {
|
|
33
|
+
// first note all the currently used keys in an array, used like a map :3
|
|
34
|
+
// note that this automatically updates the array's length property to
|
|
35
|
+
// be one past the largest key.
|
|
36
|
+
const usedKeys = [];
|
|
37
|
+
answerForms.forEach((ans) => {
|
|
38
|
+
usedKeys[ans.key] = true;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// then scan through the array to find the first unused (undefined) key
|
|
42
|
+
for (let i = 0; i < usedKeys.length; i++) {
|
|
43
|
+
if (!usedKeys[i]) {
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// if we didn't find a key, make one bigger than all the other keys,
|
|
49
|
+
// since that's how the length property is defined to work on arrays
|
|
50
|
+
return usedKeys.length;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
class ExpressionEditor extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
54
|
+
static propTypes = {
|
|
55
|
+
...Changeable.propTypes,
|
|
56
|
+
answerForms: PropTypes.arrayOf(answerFormType),
|
|
57
|
+
times: PropTypes.bool,
|
|
58
|
+
buttonSets: TexButtons.buttonSetsType,
|
|
59
|
+
functions: PropTypes.arrayOf(PropTypes.string),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
static widgetName: "expression" = "expression";
|
|
63
|
+
|
|
64
|
+
static defaultProps: $FlowFixMe = {
|
|
65
|
+
answerForms: [],
|
|
66
|
+
times: false,
|
|
67
|
+
buttonSets: ["basic"],
|
|
68
|
+
functions: ["f", "g", "h"],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
constructor(props: $FlowFixMe) {
|
|
72
|
+
super(props);
|
|
73
|
+
// Is the format of `value` TeX or plain text?
|
|
74
|
+
// TODO(alex): Remove after backfilling everything to TeX
|
|
75
|
+
// TODO(joel) - sucks if you edit some expression without
|
|
76
|
+
// backslashes or curly braces, then come back to the question and
|
|
77
|
+
// it's surprisingly not TeX anymore.
|
|
78
|
+
|
|
79
|
+
let isTex;
|
|
80
|
+
// default to TeX if new;
|
|
81
|
+
if (props.answerForms.length === 0) {
|
|
82
|
+
isTex = true;
|
|
83
|
+
} else {
|
|
84
|
+
isTex = _(props.answerForms).any((form) => {
|
|
85
|
+
const {value} = form;
|
|
86
|
+
// only TeX has backslashes and curly braces
|
|
87
|
+
return (
|
|
88
|
+
_.indexOf(value, "\\") !== -1 ||
|
|
89
|
+
_.indexOf(value, "{") !== -1
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.state = {isTex};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
98
|
+
return Changeable.change.apply(this, args);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
render(): React.Node {
|
|
102
|
+
const answerOptions = this.props.answerForms
|
|
103
|
+
.map((obj, index) => {
|
|
104
|
+
const expressionProps = {
|
|
105
|
+
// note we're using
|
|
106
|
+
// *this.props*.{times,functions,buttonSets} since each
|
|
107
|
+
// answer area has the same settings for those
|
|
108
|
+
times: this.props.times,
|
|
109
|
+
functions: this.props.functions,
|
|
110
|
+
buttonSets: this.props.buttonSets,
|
|
111
|
+
|
|
112
|
+
buttonsVisible: "focused",
|
|
113
|
+
form: obj.form,
|
|
114
|
+
simplify: obj.simplify,
|
|
115
|
+
value: obj.value,
|
|
116
|
+
|
|
117
|
+
onChange: (props) => this.updateForm(index, props),
|
|
118
|
+
trackInteraction: () => {},
|
|
119
|
+
|
|
120
|
+
widgetId: this.props.widgetId + "-" + index,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return lens(obj)
|
|
124
|
+
.merge([], {
|
|
125
|
+
draggable: true,
|
|
126
|
+
onChange: (props) => this.updateForm(index, props),
|
|
127
|
+
onDelete: () => this.handleRemoveForm(index),
|
|
128
|
+
expressionProps: expressionProps,
|
|
129
|
+
})
|
|
130
|
+
.freeze();
|
|
131
|
+
})
|
|
132
|
+
.map((obj, index) => <AnswerOption key={index} {...obj} />);
|
|
133
|
+
|
|
134
|
+
const sortable = (
|
|
135
|
+
<SortableArea
|
|
136
|
+
components={answerOptions}
|
|
137
|
+
onReorder={this.handleReorder}
|
|
138
|
+
className="answer-options-list"
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// checkboxes to choose which sets of input buttons are shown
|
|
143
|
+
const buttonSetChoices = _(TexButtons.buttonSets).map((set, name) => {
|
|
144
|
+
// The first one gets special cased to always be checked, disabled,
|
|
145
|
+
// and float left.
|
|
146
|
+
const isFirst = name === "basic";
|
|
147
|
+
const checked = _.contains(this.props.buttonSets, name) || isFirst;
|
|
148
|
+
const className = isFirst
|
|
149
|
+
? "button-set-label-float"
|
|
150
|
+
: "button-set-label";
|
|
151
|
+
return (
|
|
152
|
+
<label className={className} key={name}>
|
|
153
|
+
<input
|
|
154
|
+
type="checkbox"
|
|
155
|
+
checked={checked}
|
|
156
|
+
disabled={isFirst}
|
|
157
|
+
onChange={() => this.handleButtonSet(name)}
|
|
158
|
+
/>
|
|
159
|
+
{name}
|
|
160
|
+
</label>
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const {TeX} = getDependencies(); // OldExpression only
|
|
165
|
+
|
|
166
|
+
buttonSetChoices.splice(
|
|
167
|
+
1,
|
|
168
|
+
1,
|
|
169
|
+
<label key="show-div">
|
|
170
|
+
<input type="checkbox" onChange={this.handleToggleDiv} />
|
|
171
|
+
<span className="show-div-button">
|
|
172
|
+
show <TeX>\div</TeX> button
|
|
173
|
+
</span>
|
|
174
|
+
</label>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="perseus-widget-expression-editor">
|
|
179
|
+
<h3 className="expression-editor-h3">Global Options</h3>
|
|
180
|
+
|
|
181
|
+
<div>
|
|
182
|
+
<PropCheckBox
|
|
183
|
+
times={this.props.times}
|
|
184
|
+
onChange={this.props.onChange}
|
|
185
|
+
labelAlignment="right"
|
|
186
|
+
label="Use × for rendering multiplication instead of a
|
|
187
|
+
center dot."
|
|
188
|
+
/>
|
|
189
|
+
<InfoTip>
|
|
190
|
+
<p>
|
|
191
|
+
For pre-algebra problems this option displays
|
|
192
|
+
multiplication as \times instead of \cdot in both
|
|
193
|
+
the rendered output and the acceptable formats
|
|
194
|
+
examples.
|
|
195
|
+
</p>
|
|
196
|
+
</InfoTip>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div>
|
|
200
|
+
<label>
|
|
201
|
+
{"Function variables: "}
|
|
202
|
+
<input
|
|
203
|
+
type="text"
|
|
204
|
+
defaultValue={this.props.functions.join(" ")}
|
|
205
|
+
onChange={this.handleFunctions}
|
|
206
|
+
/>
|
|
207
|
+
</label>
|
|
208
|
+
<InfoTip>
|
|
209
|
+
<p>
|
|
210
|
+
Single-letter variables listed here will be
|
|
211
|
+
interpreted as functions. This let us know that f(x)
|
|
212
|
+
means "f of x" and not "f times x".
|
|
213
|
+
</p>
|
|
214
|
+
</InfoTip>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div>
|
|
218
|
+
<div>Button sets:</div>
|
|
219
|
+
{buttonSetChoices}
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{this.state.isTex && (
|
|
223
|
+
<TexButtons
|
|
224
|
+
className="math-input-buttons"
|
|
225
|
+
sets={this.props.buttonSets}
|
|
226
|
+
convertDotToTimes={this.props.times}
|
|
227
|
+
// onInsert is typed as `(string | ((input: $FlowFixMe) => void)) => void`
|
|
228
|
+
// $FlowFixMe[incompatible-type]
|
|
229
|
+
onInsert={this.handleTexInsert}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
<h3 className="expression-editor-h3">Answers</h3>
|
|
234
|
+
|
|
235
|
+
<p style={{margin: "4px 0"}}>
|
|
236
|
+
student responses area matched against these from top to
|
|
237
|
+
bottom
|
|
238
|
+
</p>
|
|
239
|
+
|
|
240
|
+
{sortable}
|
|
241
|
+
|
|
242
|
+
<div>
|
|
243
|
+
<button
|
|
244
|
+
className="simple-button orange"
|
|
245
|
+
style={{fontSize: 13}}
|
|
246
|
+
onClick={this.newAnswer}
|
|
247
|
+
type="button"
|
|
248
|
+
>
|
|
249
|
+
Add new answer
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
serialize: () => $FlowFixMe = () => {
|
|
257
|
+
const formSerializables = [
|
|
258
|
+
"value",
|
|
259
|
+
"form",
|
|
260
|
+
"simplify",
|
|
261
|
+
"considered",
|
|
262
|
+
// it's a little weird to serialize the react key, but saves some
|
|
263
|
+
// effort reconstructing them when this item is loaded later.
|
|
264
|
+
"key",
|
|
265
|
+
];
|
|
266
|
+
const serializables = [
|
|
267
|
+
"answerForms",
|
|
268
|
+
"buttonSets",
|
|
269
|
+
"functions",
|
|
270
|
+
"times",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const answerForms = this.props.answerForms.map((form) => {
|
|
274
|
+
return _(form).pick(formSerializables);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return lens(this.props)
|
|
278
|
+
.set(["answerForms"], answerForms)
|
|
279
|
+
.mod([], (props) => _(props).pick(serializables))
|
|
280
|
+
.freeze();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
getSaveWarnings: () => $FlowFixMe = () => {
|
|
284
|
+
const issues = [];
|
|
285
|
+
|
|
286
|
+
if (this.props.answerForms.length === 0) {
|
|
287
|
+
issues.push("No answers specified");
|
|
288
|
+
} else {
|
|
289
|
+
const hasCorrect = !!_(this.props.answerForms).find((form) => {
|
|
290
|
+
return form.considered === "correct";
|
|
291
|
+
});
|
|
292
|
+
if (!hasCorrect) {
|
|
293
|
+
issues.push("No correct answer specified");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_(this.props.answerForms).each((form, ix) => {
|
|
297
|
+
if (this.props.value === "") {
|
|
298
|
+
issues.push(`Answer ${ix + 1} is empty`);
|
|
299
|
+
} else {
|
|
300
|
+
// note we're not using icu for content creators
|
|
301
|
+
const expression = KAS.parse(form.value, {
|
|
302
|
+
functions: this.props.functions,
|
|
303
|
+
});
|
|
304
|
+
if (!expression.parsed) {
|
|
305
|
+
issues.push(`Couldn't parse ${form.value}`);
|
|
306
|
+
} else if (
|
|
307
|
+
form.simplify &&
|
|
308
|
+
!expression.expr.isSimplified()
|
|
309
|
+
) {
|
|
310
|
+
issues.push(
|
|
311
|
+
`${form.value} isn't simplified, but is required" +
|
|
312
|
+
" to be`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// TODO(joel) - warn about:
|
|
319
|
+
// - unreachable answers (how??)
|
|
320
|
+
// - specific answers following unspecific answers
|
|
321
|
+
// - incorrect answers as the final form
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return issues;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
_newEmptyAnswerForm: () => $FlowFixMe = () => {
|
|
328
|
+
return {
|
|
329
|
+
considered: "correct",
|
|
330
|
+
form: false,
|
|
331
|
+
|
|
332
|
+
// note: the key means "n-th form created" - not "form in
|
|
333
|
+
// position n" and will stay the same for the life of this form
|
|
334
|
+
key: _makeNewKey(this.props.answerForms),
|
|
335
|
+
|
|
336
|
+
simplify: false,
|
|
337
|
+
value: "",
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
newAnswer: () => void = () => {
|
|
342
|
+
const answerForms = this.props.answerForms.slice();
|
|
343
|
+
answerForms.push(this._newEmptyAnswerForm());
|
|
344
|
+
this.change({answerForms});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
handleRemoveForm: (number) => void = (i) => {
|
|
348
|
+
const answerForms = this.props.answerForms.slice();
|
|
349
|
+
answerForms.splice(i, 1);
|
|
350
|
+
this.change({answerForms});
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// called when the options (including the expression itself) to an answer
|
|
354
|
+
// form change
|
|
355
|
+
updateForm: (number, $FlowFixMe) => void = (i, props) => {
|
|
356
|
+
const answerForms = lens(this.props.answerForms)
|
|
357
|
+
.merge([i], props)
|
|
358
|
+
.freeze();
|
|
359
|
+
|
|
360
|
+
this.change({answerForms});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
handleReorder: ($FlowFixMe) => void = (components) => {
|
|
364
|
+
const answerForms = _(components).map((component) => {
|
|
365
|
+
const form = _(component.props).pick(
|
|
366
|
+
"considered",
|
|
367
|
+
"form",
|
|
368
|
+
"simplify",
|
|
369
|
+
"value",
|
|
370
|
+
);
|
|
371
|
+
form.key = component.key;
|
|
372
|
+
return form;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
this.change({answerForms});
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// called when the selected buttonset changes
|
|
379
|
+
handleButtonSet: (string) => void = (changingName) => {
|
|
380
|
+
const buttonSetNames = _(TexButtons.buttonSets).keys();
|
|
381
|
+
|
|
382
|
+
// Filter to preserve order - using .union and .difference would always
|
|
383
|
+
// move the last added button set to the end.
|
|
384
|
+
const buttonSets = _(buttonSetNames).filter((set) => {
|
|
385
|
+
return (
|
|
386
|
+
_(this.props.buttonSets).contains(set) !==
|
|
387
|
+
(set === changingName)
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
this.props.onChange({buttonSets});
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
handleToggleDiv: () => void = () => {
|
|
395
|
+
// We always want buttonSets to contain exactly one of "basic" and
|
|
396
|
+
// "basic+div". Toggle between the two of them.
|
|
397
|
+
// If someone can think of a more elegant formulation of this (there
|
|
398
|
+
// must be one!) feel free to change it.
|
|
399
|
+
let keep, remove;
|
|
400
|
+
if (_(this.props.buttonSets).contains("basic+div")) {
|
|
401
|
+
keep = "basic";
|
|
402
|
+
remove = "basic+div";
|
|
403
|
+
} else {
|
|
404
|
+
keep = "basic+div";
|
|
405
|
+
remove = "basic";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const buttonSets = _(this.props.buttonSets)
|
|
409
|
+
.reject((set) => set === remove)
|
|
410
|
+
.concat(keep);
|
|
411
|
+
|
|
412
|
+
this.change("buttonSets", buttonSets);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// called when the correct answer changes
|
|
416
|
+
handleTexInsert: (string) => void = (str) => {
|
|
417
|
+
// eslint-disable-next-line react/no-string-refs
|
|
418
|
+
this.refs.expression.insert(str);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// called when the function variables change
|
|
422
|
+
handleFunctions: (SyntheticInputEvent<>) => void = (e) => {
|
|
423
|
+
const newProps = {};
|
|
424
|
+
newProps.functions = _.compact(e.target.value.split(/[ ,]+/));
|
|
425
|
+
this.props.onChange(newProps);
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Find the next element in arr after val, wrapping around to the first.
|
|
430
|
+
const findNextIn = function (arr, val) {
|
|
431
|
+
let ix = _(arr).indexOf(val);
|
|
432
|
+
ix = (ix + 1) % arr.length;
|
|
433
|
+
return arr[ix];
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
class AnswerOption extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
437
|
+
static propTypes = {
|
|
438
|
+
...Changeable.propTypes,
|
|
439
|
+
considered: PropTypes.oneOf(CONSIDERED).isRequired,
|
|
440
|
+
expressionProps: PropTypes.object.isRequired,
|
|
441
|
+
|
|
442
|
+
// Must the answer have the same form as this answer.
|
|
443
|
+
form: PropTypes.bool.isRequired,
|
|
444
|
+
|
|
445
|
+
// Must the answer be simplified.
|
|
446
|
+
simplify: PropTypes.bool.isRequired,
|
|
447
|
+
|
|
448
|
+
onChange: PropTypes.func.isRequired,
|
|
449
|
+
onDelete: PropTypes.func.isRequired,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
state: $FlowFixMe = {deleteFocused: false};
|
|
453
|
+
|
|
454
|
+
handleDeleteBlur = () => {
|
|
455
|
+
this.setState({deleteFocused: false});
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
change = (...args) => {
|
|
459
|
+
return Changeable.change.apply(this, args);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
render(): React.Node {
|
|
463
|
+
let removeButton = null;
|
|
464
|
+
if (this.state.deleteFocused) {
|
|
465
|
+
removeButton = (
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
className="simple-button orange"
|
|
469
|
+
onClick={this.handleImSure}
|
|
470
|
+
onBlur={this.handleDeleteBlur}
|
|
471
|
+
>
|
|
472
|
+
I'm sure!
|
|
473
|
+
</button>
|
|
474
|
+
);
|
|
475
|
+
} else {
|
|
476
|
+
removeButton = (
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className="simple-button orange"
|
|
480
|
+
onClick={this.handleDelete}
|
|
481
|
+
>
|
|
482
|
+
Delete
|
|
483
|
+
</button>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<div className="expression-answer-option">
|
|
489
|
+
<div className="answer-handle" />
|
|
490
|
+
|
|
491
|
+
<div className="answer-body">
|
|
492
|
+
<div className="answer-considered">
|
|
493
|
+
<div
|
|
494
|
+
onClick={this.toggleConsidered}
|
|
495
|
+
className={"answer-status " + this.props.considered}
|
|
496
|
+
>
|
|
497
|
+
{this.props.considered}
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<div className="answer-expression">
|
|
501
|
+
<Expression {...this.props.expressionProps} />
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<div className="answer-option">
|
|
506
|
+
<PropCheckBox
|
|
507
|
+
form={this.props.form}
|
|
508
|
+
onChange={this.props.onChange}
|
|
509
|
+
labelAlignment="right"
|
|
510
|
+
label="Answer expression must have the same form."
|
|
511
|
+
/>
|
|
512
|
+
<InfoTip>
|
|
513
|
+
<p>
|
|
514
|
+
The student's answer must be in the same form.
|
|
515
|
+
Commutativity and excess negative signs are
|
|
516
|
+
ignored.
|
|
517
|
+
</p>
|
|
518
|
+
</InfoTip>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<div className="answer-option">
|
|
522
|
+
<PropCheckBox
|
|
523
|
+
simplify={this.props.simplify}
|
|
524
|
+
onChange={this.props.onChange}
|
|
525
|
+
labelAlignment="right"
|
|
526
|
+
label="Answer expression must be fully expanded and
|
|
527
|
+
simplified."
|
|
528
|
+
/>
|
|
529
|
+
<InfoTip>
|
|
530
|
+
<p>
|
|
531
|
+
The student's answer must be fully expanded and
|
|
532
|
+
simplified. Answering this equation (x^2+2x+1)
|
|
533
|
+
with this factored equation (x+1)^2 will render
|
|
534
|
+
this response "Your answer is not fully expanded
|
|
535
|
+
and simplified."
|
|
536
|
+
</p>
|
|
537
|
+
</InfoTip>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div className="remove-container">{removeButton}</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
handleImSure = () => {
|
|
547
|
+
this.props.onDelete();
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
handleDelete = () => {
|
|
551
|
+
this.setState({deleteFocused: true});
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
toggleConsidered = () => {
|
|
555
|
+
const newVal = findNextIn(CONSIDERED, this.props.considered);
|
|
556
|
+
this.change({considered: newVal});
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export default ExpressionEditor;
|