@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,554 @@
1
+ /* eslint-disable jsx-a11y/anchor-is-valid, react/sort-comp */
2
+ // @flow
3
+ import {
4
+ components,
5
+ icons,
6
+ Changeable,
7
+ EditorJsonify,
8
+ Util,
9
+ } from "@khanacademy/perseus";
10
+ import createReactClass from "create-react-class";
11
+ import * as React from "react";
12
+ import _ from "underscore";
13
+
14
+ import Editor from "../editor.jsx";
15
+
16
+ const {
17
+ ButtonGroup,
18
+ InfoTip,
19
+ InlineIcon,
20
+ MultiButtonGroup,
21
+ NumberInput,
22
+ PropCheckBox,
23
+ TextInput,
24
+ } = components;
25
+ const {iconGear, iconTrash} = icons;
26
+ const {firstNumericalParse} = Util;
27
+
28
+ const answerFormButtons = [
29
+ {title: "Integers", value: "integer", content: "6"},
30
+ {title: "Decimals", value: "decimal", content: "0.75"},
31
+ {title: "Proper fractions", value: "proper", content: "\u2157"},
32
+ {
33
+ title: "Improper fractions",
34
+ value: "improper",
35
+ content: "\u2077\u2044\u2084",
36
+ },
37
+ {title: "Mixed numbers", value: "mixed", content: "1\u00BE"},
38
+ {title: "Numbers with \u03C0", value: "pi", content: "\u03C0"},
39
+ ];
40
+
41
+ const initAnswer = (status) => {
42
+ return {
43
+ value: null,
44
+ status: status,
45
+ message: "",
46
+ simplify: "required",
47
+ answerForms: [],
48
+ strict: false,
49
+ maxError: null,
50
+ };
51
+ };
52
+
53
+ const NumericInputEditor: any = createReactClass({
54
+ displayName: "NumericInputEditor",
55
+
56
+ propTypes: {
57
+ ...Changeable.propTypes,
58
+ },
59
+
60
+ statics: {
61
+ widgetName: "numeric-input",
62
+ },
63
+
64
+ getDefaultProps: function () {
65
+ return {
66
+ answers: [initAnswer("correct")],
67
+ size: "normal",
68
+ coefficient: false,
69
+ labelText: "",
70
+ multipleNumberInput: false,
71
+ rightAlign: false,
72
+ };
73
+ },
74
+
75
+ getInitialState: function () {
76
+ return {
77
+ lastStatus: "wrong",
78
+ showOptions: _.map(this.props.answers, () => false),
79
+ };
80
+ },
81
+
82
+ render: function () {
83
+ const answers = this.props.answers;
84
+
85
+ const unsimplifiedAnswers = (i) => (
86
+ <div className="perseus-widget-row">
87
+ <label>Unsimplified answers are</label>
88
+ <ButtonGroup
89
+ value={answers[i]["simplify"]}
90
+ allowEmpty={false}
91
+ buttons={[
92
+ {value: "required", content: "ungraded"},
93
+ {value: "optional", content: "accepted"},
94
+ {value: "enforced", content: "wrong"},
95
+ ]}
96
+ onChange={this.updateAnswer(i, "simplify")}
97
+ />
98
+ <InfoTip>
99
+ <p>
100
+ Normally select "ungraded". This will give the user a
101
+ message saying the answer is correct but not simplified.
102
+ The user will then have to simplify it and re-enter, but
103
+ will not be penalized. (5th grade and after)
104
+ </p>
105
+ <p>
106
+ Select "accepted" only if the user is not expected to
107
+ know how to simplify fractions yet. (Anything prior to
108
+ 5th grade)
109
+ </p>
110
+ <p>
111
+ Select "wrong" <em>only</em> if we are specifically
112
+ assessing the ability to simplify.
113
+ </p>
114
+ </InfoTip>
115
+ </div>
116
+ );
117
+
118
+ const suggestedAnswerTypes = (i) => (
119
+ <div>
120
+ <div className="perseus-widget-row">
121
+ <label>Choose the suggested answer formats</label>
122
+ <MultiButtonGroup
123
+ buttons={answerFormButtons}
124
+ values={answers[i]["answerForms"]}
125
+ onChange={this.updateAnswer(i, "answerForms")}
126
+ />
127
+ <InfoTip>
128
+ <p>
129
+ Formats will be autoselected for you based on the
130
+ given answer; to show no suggested formats and
131
+ accept all types, simply have a decimal/integer be
132
+ the answer. Values with &pi; will have format "pi",
133
+ and values that are fractions will have some subset
134
+ (mixed will be "mixed" and "proper"; improper/proper
135
+ will both be "improper" and "proper"). If you would
136
+ like to specify that it is only a proper fraction
137
+ (or only a mixed/improper fraction), deselect the
138
+ other format. Except for specific cases, you should
139
+ not need to change the autoselected formats.
140
+ </p>
141
+ <p>
142
+ To restrict the answer to <em>only</em> an improper
143
+ fraction (i.e. 7/4), select the improper fraction
144
+ and toggle "strict" to true. This <b>will not</b>{" "}
145
+ accept 1.75 as an answer.{" "}
146
+ </p>
147
+ <p>
148
+ Unless you are testing that specific skill, please
149
+ do not restrict the answer format.
150
+ </p>
151
+ </InfoTip>
152
+ </div>
153
+ <div className="perseus-widget-row">
154
+ <PropCheckBox
155
+ label="Strictly match only these formats"
156
+ strict={answers[i]["strict"]}
157
+ // eslint-disable-next-line react/jsx-no-bind
158
+ onChange={this.updateAnswer.bind(this, i)}
159
+ />
160
+ </div>
161
+ </div>
162
+ );
163
+
164
+ const maxError = (i) => (
165
+ <div className="perseus-widget-row">
166
+ <label>
167
+ Max error{" "}
168
+ <NumberInput
169
+ className="max-error"
170
+ value={answers[i]["maxError"]}
171
+ onChange={this.updateAnswer(i, "maxError")}
172
+ placeholder="0"
173
+ />
174
+ </label>
175
+ </div>
176
+ );
177
+
178
+ const inputSize = (
179
+ <div className="perseus-widget-row">
180
+ <label>Width: </label>
181
+ <ButtonGroup
182
+ value={this.props.size}
183
+ allowEmpty={false}
184
+ buttons={[
185
+ {value: "normal", content: "Normal (80px)"},
186
+ {value: "small", content: "Small (40px)"},
187
+ ]}
188
+ onChange={this.change("size")}
189
+ />
190
+ <InfoTip>
191
+ <p>
192
+ Use size "Normal" for all text boxes, unless there are
193
+ multiple text boxes in one line and the answer area is
194
+ too narrow to fit them.
195
+ </p>
196
+ </InfoTip>
197
+ </div>
198
+ );
199
+
200
+ const rightAlign = (
201
+ <div className="perseus-widget-row">
202
+ <PropCheckBox
203
+ label="Right alignment"
204
+ rightAlign={this.props.rightAlign}
205
+ onChange={this.props.onChange}
206
+ />
207
+ </div>
208
+ );
209
+
210
+ const labelText = (
211
+ <div className="perseus-widget-row">
212
+ <label>
213
+ Label text:{" "}
214
+ <TextInput
215
+ value={this.props.labelText}
216
+ onChange={this.change("labelText")}
217
+ />
218
+ </label>
219
+ <InfoTip>
220
+ <p>
221
+ Text to describe this input. This will be shown to users
222
+ using screenreaders.
223
+ </p>
224
+ </InfoTip>
225
+ </div>
226
+ );
227
+
228
+ const coefficientCheck = (
229
+ <div>
230
+ <div className="perseus-widget-row">
231
+ <PropCheckBox
232
+ label="Coefficient"
233
+ coefficient={this.props.coefficient}
234
+ onChange={this.props.onChange}
235
+ />
236
+ <InfoTip>
237
+ <p>
238
+ A coefficient style number allows the student to use
239
+ - for -1 and an empty string to mean 1.
240
+ </p>
241
+ </InfoTip>
242
+ </div>
243
+ </div>
244
+ );
245
+
246
+ const addAnswerButton = (
247
+ <div>
248
+ <a
249
+ href="javascript:void(0)"
250
+ className="simple-button orange"
251
+ onClick={() => this.addAnswer()}
252
+ onKeyDown={(e) => this.onSpace(e, this.addAnswer)}
253
+ >
254
+ <span>Add new answer</span>
255
+ </a>
256
+ </div>
257
+ );
258
+
259
+ const instructions = {
260
+ wrong: "(address the mistake/misconception)",
261
+ ungraded: "(explain in detail to avoid confusion)",
262
+ correct: "(reinforce the user's understanding)",
263
+ };
264
+
265
+ const generateInputAnswerEditors = () =>
266
+ answers.map((answer, i) => {
267
+ const editor = (
268
+ <Editor
269
+ apiOptions={this.props.apiOptions}
270
+ content={answer.message || ""}
271
+ placeholder={
272
+ "Why is this answer " +
273
+ answer.status +
274
+ "?\t" +
275
+ instructions[answer.status]
276
+ }
277
+ widgetEnabled={false}
278
+ onChange={(newProps) => {
279
+ if ("content" in newProps) {
280
+ this.updateAnswer(i, {
281
+ message: newProps.content,
282
+ });
283
+ }
284
+ }}
285
+ />
286
+ );
287
+ return (
288
+ <div className="perseus-widget-row" key={i}>
289
+ <div
290
+ className={
291
+ "input-answer-editor-value-container" +
292
+ (answer.maxError ? " with-max-error" : "")
293
+ }
294
+ >
295
+ <NumberInput
296
+ value={answer.value}
297
+ className="numeric-input-value"
298
+ placeholder="answer"
299
+ format={_.last(answer.answerForms)}
300
+ onFormatChange={(newValue, format) => {
301
+ // NOTE(charlie): The mobile web expression
302
+ // editor relies on this automatic answer
303
+ // form resolution for determining when to
304
+ // show the Pi symbol. If we get rid of it,
305
+ // we should also disable Pi for
306
+ // NumericInput and require problems that
307
+ // use Pi to build on Expression.
308
+ // Alternatively, we could store answers
309
+ // as plaintext and parse them to determine
310
+ // whether or not to reveal Pi on the
311
+ // keypad (right now, answers are stored as
312
+ // resolved values, like '0.125' rather
313
+ // than '1/8').
314
+ let forms;
315
+ if (format === "pi") {
316
+ forms = ["pi"];
317
+ } else if (format === "mixed") {
318
+ forms = ["proper", "mixed"];
319
+ } else if (
320
+ format === "proper" ||
321
+ format === "improper"
322
+ ) {
323
+ forms = ["proper", "improper"];
324
+ }
325
+ this.updateAnswer(i, {
326
+ value: firstNumericalParse(newValue),
327
+ answerForms: forms,
328
+ });
329
+ }}
330
+ onChange={(newValue) => {
331
+ this.updateAnswer(i, {
332
+ value: firstNumericalParse(newValue),
333
+ });
334
+ }}
335
+ />
336
+ {answer.strict && (
337
+ <div
338
+ className="is-strict-indicator"
339
+ title="strictly equivalent to"
340
+ >
341
+ &equiv;
342
+ </div>
343
+ )}
344
+ {answer.simplify !== "required" &&
345
+ answer.status === "correct" && (
346
+ <div
347
+ className={
348
+ "simplify-indicator " +
349
+ answer.simplify
350
+ }
351
+ title="accepts unsimplified answers"
352
+ >
353
+ &permil;
354
+ </div>
355
+ )}
356
+ {answer.maxError ? (
357
+ <div className="max-error-container">
358
+ <div className="max-error-plusmn">
359
+ &plusmn;
360
+ </div>
361
+ <NumberInput
362
+ placeholder={0}
363
+ value={answers[i]["maxError"]}
364
+ format={_.last(answer.answerForms)}
365
+ onChange={this.updateAnswer(
366
+ i,
367
+ "maxError",
368
+ )}
369
+ />
370
+ </div>
371
+ ) : null}
372
+ <div className="value-divider" />
373
+ <a
374
+ href="javascript:void(0)"
375
+ className={"answer-status " + answer.status}
376
+ onClick={() => this.onStatusChange(i)}
377
+ onKeyDown={(e) =>
378
+ this.onSpace(e, this.onStatusChange, i)
379
+ }
380
+ >
381
+ {answer.status}
382
+ </a>
383
+ <a
384
+ href="javascript:void(0)"
385
+ className="answer-trash"
386
+ onClick={() => this.onTrashAnswer(i)}
387
+ onKeyDown={(e) =>
388
+ this.onSpace(e, this.onTrashAnswer, i)
389
+ }
390
+ >
391
+ <InlineIcon {...iconTrash} />
392
+ </a>
393
+ <a
394
+ href="javascript:void(0)"
395
+ className="options-toggle"
396
+ onClick={() => this.onToggleOptions(i)}
397
+ onKeyDown={(e) =>
398
+ this.onSpace(e, this.onToggleOptions, i)
399
+ }
400
+ >
401
+ <InlineIcon {...iconGear} />
402
+ </a>
403
+ </div>
404
+ <div className="input-answer-editor-message">
405
+ {editor}
406
+ </div>
407
+ {this.state.showOptions[i] && (
408
+ <div className="options-container">
409
+ {maxError(i)}
410
+ {answer.status === "correct" &&
411
+ unsimplifiedAnswers(i)}
412
+ {suggestedAnswerTypes(i)}
413
+ </div>
414
+ )}
415
+ </div>
416
+ );
417
+ });
418
+
419
+ return (
420
+ <div className="perseus-input-number-editor">
421
+ <div ref={(e) => (this.multInputOption = e)}>
422
+ <select onChange={this.onMultipleInputChange}>
423
+ <option value="simple-numeric-input">
424
+ Ask for one correct solution
425
+ </option>
426
+ <option value="multiple-numeric-input">
427
+ Ask for all correct solutions
428
+ </option>
429
+ </select>
430
+ </div>
431
+ <div className="ui-title">User input</div>
432
+ <div className="msg-title">
433
+ Message shown to user on attempt
434
+ </div>
435
+ {generateInputAnswerEditors()}
436
+ {addAnswerButton}
437
+ {inputSize}
438
+ {rightAlign}
439
+ {coefficientCheck}
440
+ {labelText}
441
+ </div>
442
+ );
443
+ },
444
+
445
+ change(...args) {
446
+ // $FlowFixMe[object-this-reference]
447
+ return Changeable.change.apply(this, args);
448
+ },
449
+
450
+ onToggleOptions: function (choiceIndex) {
451
+ const showOptions = this.state.showOptions.slice();
452
+ showOptions[choiceIndex] = !showOptions[choiceIndex];
453
+ this.setState({showOptions: showOptions});
454
+ },
455
+
456
+ onTrashAnswer: function (choiceIndex) {
457
+ if (choiceIndex >= 0 && choiceIndex < this.props.answers.length) {
458
+ const answers = this.props.answers.slice(0);
459
+ answers.splice(choiceIndex, 1);
460
+ this.props.onChange({answers: answers});
461
+ }
462
+ },
463
+
464
+ onSpace: function (e, callback) {
465
+ if (e.key === " ") {
466
+ e.preventDefault(); // prevent page shifting
467
+ const args = _.toArray(arguments).slice(2);
468
+ callback.apply(this, args);
469
+ }
470
+ },
471
+
472
+ onStatusChange: function (choiceIndex) {
473
+ const statuses = ["wrong", "ungraded", "correct"];
474
+ const answers = this.props.answers;
475
+ const i = _.indexOf(statuses, answers[choiceIndex].status);
476
+ const newStatus = statuses[(i + 1) % statuses.length];
477
+
478
+ this.updateAnswer(choiceIndex, {
479
+ status: newStatus,
480
+ simplify: newStatus === "correct" ? "required" : "accepted",
481
+ });
482
+ },
483
+
484
+ onMultipleInputChange: function (event) {
485
+ const newOption = event.target.value;
486
+ if (newOption === "multiple-numeric-input") {
487
+ this.props.onChange({multipleNumberInput: true});
488
+ } else {
489
+ this.props.onChange({multipleNumberInput: false});
490
+ }
491
+ },
492
+
493
+ updateAnswer: function (choiceIndex, update) {
494
+ if (!_.isObject(update)) {
495
+ return _.partial(
496
+ (choiceIndex, key, value) => {
497
+ const update = {};
498
+ update[key] = value;
499
+ this.updateAnswer(choiceIndex, update);
500
+ },
501
+ choiceIndex,
502
+ update,
503
+ );
504
+ }
505
+
506
+ let answers = _.clone(this.props.answers);
507
+
508
+ // Don't bother to make a new answer box unless we are editing the last
509
+ // one.
510
+ // TODO(oliver): This might not be necessary anymore.
511
+ if (choiceIndex === answers.length) {
512
+ const lastAnswer = initAnswer(this.state.lastStatus);
513
+ answers = answers.concat(lastAnswer);
514
+ }
515
+
516
+ answers[choiceIndex] = _.extend({}, answers[choiceIndex], update);
517
+ this.props.onChange({answers: answers});
518
+ },
519
+
520
+ addAnswer: function () {
521
+ const lastAnswer = initAnswer(this.state.lastStatus);
522
+ const answers = this.props.answers.concat(lastAnswer);
523
+ this.props.onChange({answers: answers});
524
+ },
525
+
526
+ getSaveWarnings: function () {
527
+ // Filter out all the empty answers
528
+ const warnings = [];
529
+ // TODO(emily): This doesn't actually work, because the value is either
530
+ // null or undefined when undefined, probably.
531
+ if (_.contains(_.pluck(this.props.answers, "value"), "")) {
532
+ warnings.push("One or more answers is empty");
533
+ }
534
+ this.props.answers.forEach((answer, i) => {
535
+ const formatError =
536
+ answer.strict &&
537
+ (!answer.answerForms || answer.answerForms.length === 0);
538
+ if (formatError) {
539
+ warnings.push(
540
+ `Answer ${i + 1} is set to string format ` +
541
+ "matching, but no format was selected",
542
+ );
543
+ }
544
+ });
545
+ return warnings;
546
+ },
547
+
548
+ serialize() {
549
+ // $FlowFixMe[object-this-reference]
550
+ return EditorJsonify.serialize.call(this);
551
+ },
552
+ });
553
+
554
+ export default NumericInputEditor;