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