@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.
Files changed (127) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/es/index.css +1020 -0
  4. package/dist/es/index.js +9 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +1020 -0
  7. package/dist/index.js +9 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +6 -1
  10. package/package.json +16 -4
  11. package/src/README.md +7 -0
  12. package/src/__stories__/editor.stories.js +33 -0
  13. package/src/__stories__/section-control-button.stories.jsx +27 -0
  14. package/src/__testdata__/input-number_testdata.js +28 -0
  15. package/src/__tests__/editor_test.jsx +123 -0
  16. package/src/__tests__/i18n_test.jsx +585 -0
  17. package/src/__tests__/traversal_test.jsx +378 -0
  18. package/src/all-editors.js +85 -0
  19. package/src/all-editors_test.js +17 -0
  20. package/src/all-widgets.js +10 -0
  21. package/src/article-editor.jsx +437 -0
  22. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  23. package/src/components/blur-input.jsx +62 -0
  24. package/src/components/checkbox.jsx +194 -0
  25. package/src/components/device-framer.jsx +91 -0
  26. package/src/components/drag-target.jsx +88 -0
  27. package/src/components/dropdown-option.jsx +318 -0
  28. package/src/components/form-wrapped-text-field.jsx +248 -0
  29. package/src/components/graph-settings.jsx +631 -0
  30. package/src/components/hover-behavior.jsx +178 -0
  31. package/src/components/json-editor.jsx +121 -0
  32. package/src/components/link.jsx +173 -0
  33. package/src/components/simple-button.jsx +86 -0
  34. package/src/components/sortable.jsx +186 -0
  35. package/src/components/util.js +39 -0
  36. package/src/components/viewport-resizer.jsx +73 -0
  37. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  38. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  39. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  40. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  41. package/src/diffs/__tests__/split-diff_test.js +34 -0
  42. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  43. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  44. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  45. package/src/diffs/article-diff.jsx +70 -0
  46. package/src/diffs/item-diff.jsx +77 -0
  47. package/src/diffs/renderer-diff.jsx +143 -0
  48. package/src/diffs/split-diff.jsx +30 -0
  49. package/src/diffs/string-array-diff.jsx +75 -0
  50. package/src/diffs/structured-item-diff.jsx +269 -0
  51. package/src/diffs/tags-diff.jsx +50 -0
  52. package/src/diffs/text-diff.jsx +194 -0
  53. package/src/diffs/widget-diff-performer.jsx +74 -0
  54. package/src/diffs/widget-diff.jsx +291 -0
  55. package/src/editor-page.jsx +351 -0
  56. package/src/editor.jsx +1364 -0
  57. package/src/hint-editor.jsx +447 -0
  58. package/src/i18n.jsx +139 -0
  59. package/src/iframe-content-renderer.jsx +201 -0
  60. package/src/index.js +27 -8
  61. package/src/item-editor.jsx +159 -0
  62. package/src/item-extras-editor.jsx +150 -0
  63. package/src/katex-error-view.jsx +87 -0
  64. package/src/multirenderer-editor.jsx +1074 -0
  65. package/src/section-control-button.jsx +53 -0
  66. package/src/stateful-article-editor.jsx +176 -0
  67. package/src/stateful-editor-page.jsx +90 -0
  68. package/src/styles/perseus-editor.less +1339 -0
  69. package/src/testing-widgets.js +12 -0
  70. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  71. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  72. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  73. package/src/widgets/categorizer-editor.jsx +103 -0
  74. package/src/widgets/cs-program-editor.jsx +258 -0
  75. package/src/widgets/definition-editor.jsx +75 -0
  76. package/src/widgets/dropdown-editor.jsx +201 -0
  77. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  78. package/src/widgets/example-graphie-widget.jsx +175 -0
  79. package/src/widgets/example-widget-editor.jsx +60 -0
  80. package/src/widgets/example-widget.jsx +153 -0
  81. package/src/widgets/explanation-editor.jsx +89 -0
  82. package/src/widgets/expression-editor.jsx +560 -0
  83. package/src/widgets/graded-group-editor.jsx +162 -0
  84. package/src/widgets/graded-group-set-editor.jsx +101 -0
  85. package/src/widgets/grapher-editor.jsx +154 -0
  86. package/src/widgets/group-editor.jsx +83 -0
  87. package/src/widgets/iframe-editor.jsx +200 -0
  88. package/src/widgets/image-editor.jsx +326 -0
  89. package/src/widgets/input-number-editor.jsx +269 -0
  90. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  91. package/src/widgets/interaction-editor.jsx +1389 -0
  92. package/src/widgets/interactive-graph-editor.jsx +322 -0
  93. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  94. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  95. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  96. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  97. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  98. package/src/widgets/label-image/answer-choices.jsx +231 -0
  99. package/src/widgets/label-image/behavior.jsx +92 -0
  100. package/src/widgets/label-image/marker.jsx +292 -0
  101. package/src/widgets/label-image/question-markers.jsx +181 -0
  102. package/src/widgets/label-image/select-image.jsx +86 -0
  103. package/src/widgets/label-image-editor.jsx +257 -0
  104. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  105. package/src/widgets/matcher-editor.jsx +147 -0
  106. package/src/widgets/matrix-editor.jsx +155 -0
  107. package/src/widgets/measurer-editor.jsx +245 -0
  108. package/src/widgets/molecule-editor.jsx +63 -0
  109. package/src/widgets/number-line-editor.jsx +498 -0
  110. package/src/widgets/numeric-input-editor.jsx +554 -0
  111. package/src/widgets/orderer-editor.jsx +166 -0
  112. package/src/widgets/passage-editor.jsx +115 -0
  113. package/src/widgets/passage-ref-editor.jsx +77 -0
  114. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  115. package/src/widgets/plotter-editor.jsx +537 -0
  116. package/src/widgets/radio/editor.jsx +398 -0
  117. package/src/widgets/reaction-diagram-editor.jsx +127 -0
  118. package/src/widgets/sequence-editor.jsx +205 -0
  119. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  120. package/src/widgets/simple-markdown-tester.jsx +91 -0
  121. package/src/widgets/simulator-editor.jsx +144 -0
  122. package/src/widgets/sorter-editor.jsx +100 -0
  123. package/src/widgets/table-editor.jsx +164 -0
  124. package/src/widgets/transformer-editor.jsx +1006 -0
  125. package/src/widgets/unit-editor.jsx +293 -0
  126. package/src/widgets/video-editor.jsx +74 -0
  127. 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;