@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,326 @@
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 BlurInput from "../components/blur-input.jsx";
15
+ import Editor from "../editor.jsx";
16
+
17
+ const {InfoTip, InlineIcon, RangeInput} = components;
18
+
19
+ const defaultBoxSize = 400;
20
+ const defaultRange = [0, 10];
21
+ const defaultBackgroundImage = {
22
+ url: null,
23
+ width: 0,
24
+ height: 0,
25
+ };
26
+
27
+ // Match any image URL (including "web+graphie" links) that is hosted by KA.
28
+ // We're somewhat generous in our AWS URL matching
29
+ // ("ka-<something>.s3.amazonaws.com") so that we don't have to update Perseus
30
+ // every time we add a new proxied AWS bucket.
31
+ const INTERNALLY_HOSTED_DOMAINS =
32
+ "(" +
33
+ "ka-.*.s3.amazonaws.com|" +
34
+ "(fastly|cdn).kastatic.org|" +
35
+ "khanacademy.org|" +
36
+ "kasandbox.org" +
37
+ ")";
38
+ const INTERNALLY_HOSTED_URL_RE = new RegExp(
39
+ "^(https?|web\\+graphie)://[^/]*" + INTERNALLY_HOSTED_DOMAINS,
40
+ );
41
+
42
+ /**
43
+ * Alignment option for captions, relative to specified coordinates.
44
+ */
45
+ const captionAlignments = [
46
+ "center",
47
+ "above",
48
+ "above right",
49
+ "right",
50
+ "below right",
51
+ "below",
52
+ "below left",
53
+ "left",
54
+ "above left",
55
+ ];
56
+
57
+ const ImageEditor: $FlowFixMe = createReactClass({
58
+ displayName: "ImageEditor",
59
+
60
+ propTypes: {
61
+ ...Changeable.propTypes,
62
+ },
63
+
64
+ statics: {
65
+ widgetName: "image",
66
+ },
67
+
68
+ componentDidMount: function () {
69
+ // TODO(scottgrant): This is a hack to remove the deprecated call to
70
+ // this.isMounted() but is still considered an anti-pattern.
71
+ this._isMounted = true;
72
+ },
73
+
74
+ componentWillUnmount: function () {
75
+ this._isMounted = false;
76
+ },
77
+
78
+ getDefaultProps: function () {
79
+ return {
80
+ title: "",
81
+ range: [defaultRange, defaultRange],
82
+ box: [defaultBoxSize, defaultBoxSize],
83
+ backgroundImage: defaultBackgroundImage,
84
+ labels: [],
85
+ alt: "",
86
+ caption: "",
87
+ };
88
+ },
89
+
90
+ getInitialState: function () {
91
+ return {
92
+ backgroundImageError: "",
93
+ };
94
+ },
95
+
96
+ render: function () {
97
+ const backgroundImage = this.props.backgroundImage;
98
+
99
+ const imageSettings = (
100
+ <div className="image-settings">
101
+ <div>
102
+ <label>
103
+ <div>
104
+ Alt text:
105
+ <InfoTip>
106
+ This is important for screenreaders. The content
107
+ of this alt text will be formatted as markdown
108
+ (tables, emphasis, etc. are supported).
109
+ </InfoTip>
110
+ </div>
111
+ <Editor
112
+ apiOptions={this.props.apiOptions}
113
+ content={this.props.alt}
114
+ onChange={(props) => {
115
+ if (props.content != null) {
116
+ this.change("alt", props.content);
117
+ }
118
+ }}
119
+ widgetEnabled={false}
120
+ />
121
+ </label>
122
+ </div>
123
+ <div>
124
+ <label>
125
+ <div>Caption:</div>
126
+ <Editor
127
+ apiOptions={this.props.apiOptions}
128
+ content={this.props.caption}
129
+ onChange={(props) => {
130
+ if (props.content != null) {
131
+ this.change("caption", props.content);
132
+ }
133
+ }}
134
+ widgetEnabled={false}
135
+ />
136
+ </label>
137
+ </div>
138
+ </div>
139
+ );
140
+
141
+ const backgroundImageErrorText = (
142
+ <div className="renderer-widget-error">
143
+ {this.state.backgroundImageError}
144
+ </div>
145
+ );
146
+
147
+ return (
148
+ <div className="perseus-image-editor">
149
+ <label>
150
+ Image url:
151
+ <InfoTip>Paste an image or graphie image URL.</InfoTip>
152
+ {this.state.backgroundImageError &&
153
+ backgroundImageErrorText}
154
+ <BlurInput
155
+ value={backgroundImage.url || ""}
156
+ style={{width: 332}}
157
+ onChange={(url) => this.onUrlChange(url, false)}
158
+ />
159
+ </label>
160
+
161
+ {backgroundImage.url && imageSettings}
162
+ </div>
163
+ );
164
+ },
165
+
166
+ _renderRowForLabel: function (label, i) {
167
+ return (
168
+ <tr key={i}>
169
+ <td>
170
+ <RangeInput
171
+ value={label.coordinates}
172
+ // eslint-disable-next-line react/jsx-no-bind
173
+ onChange={this.onCoordinateChange.bind(this, i)}
174
+ />
175
+ </td>
176
+ <td style={{verticalAlign: "bottom", width: "5px"}}>
177
+ <input
178
+ type="text"
179
+ className="graph-settings-axis-label"
180
+ value={label.content}
181
+ // eslint-disable-next-line react/jsx-no-bind
182
+ onChange={this.onContentChange.bind(this, i)}
183
+ />
184
+ </td>
185
+ <td>
186
+ <select
187
+ className="perseus-widget-dropdown"
188
+ value={label.alignment}
189
+ // eslint-disable-next-line react/jsx-no-bind
190
+ onChange={this.onAlignmentChange.bind(this, i)}
191
+ >
192
+ {captionAlignments.map(function (alignment, i) {
193
+ return (
194
+ <option key={"" + i} value={alignment}>
195
+ {alignment}
196
+ </option>
197
+ );
198
+ }, this)}
199
+ </select>
200
+ </td>
201
+ <td>
202
+ <a
203
+ href="#"
204
+ className="simple-button orange delete-label"
205
+ title="Remove this label"
206
+ // eslint-disable-next-line react/jsx-no-bind
207
+ onClick={this.removeLabel.bind(this, i)}
208
+ >
209
+ <InlineIcon {...icons.iconTrash} />
210
+ </a>
211
+ </td>
212
+ </tr>
213
+ );
214
+ },
215
+
216
+ change(...args) {
217
+ // $FlowFixMe[object-this-reference]
218
+ return Changeable.change.apply(this, args);
219
+ },
220
+
221
+ removeLabel: function (labelIndex, e) {
222
+ e.preventDefault();
223
+ const labels = _(this.props.labels).clone();
224
+ labels.splice(labelIndex, 1);
225
+ this.props.onChange({labels: labels});
226
+ },
227
+
228
+ onCoordinateChange: function (labelIndex, newCoordinates) {
229
+ const labels = this.props.labels.slice();
230
+ labels[labelIndex] = _.extend({}, labels[labelIndex], {
231
+ coordinates: newCoordinates,
232
+ });
233
+ this.props.onChange({labels: labels});
234
+ },
235
+
236
+ onContentChange: function (labelIndex, e) {
237
+ const newContent = e.target.value;
238
+ const labels = this.props.labels.slice();
239
+ labels[labelIndex] = _.extend({}, labels[labelIndex], {
240
+ content: newContent,
241
+ });
242
+ this.props.onChange({labels: labels});
243
+ },
244
+
245
+ onAlignmentChange: function (labelIndex, e) {
246
+ const newAlignment = e.target.value;
247
+ const labels = this.props.labels.slice();
248
+ labels[labelIndex] = _.extend({}, labels[labelIndex], {
249
+ alignment: newAlignment,
250
+ });
251
+ this.props.onChange({labels: labels});
252
+ },
253
+
254
+ setUrl: function (url, width, height, silent) {
255
+ // Because this calls into WidgetEditor._handleWidgetChange, which
256
+ // checks for this widget's ref to serialize it.
257
+ //
258
+ // Errors if you switch items before the `Image` from `onUrlChange`
259
+ // loads.
260
+ if (!this._isMounted) {
261
+ return;
262
+ }
263
+
264
+ const image = _.clone(this.props.backgroundImage);
265
+ image.url = url;
266
+ image.width = width;
267
+ image.height = height;
268
+ const box = [image.width, image.height];
269
+ this.props.onChange(
270
+ {
271
+ backgroundImage: image,
272
+ box: box,
273
+ },
274
+ null,
275
+ silent,
276
+ );
277
+ },
278
+
279
+ // silently load the image when the component mounts
280
+ // silently update url and sizes when the image loads
281
+ // noisily load the image in response to the author changing it
282
+ onUrlChange: function (url, silent) {
283
+ // All article content must be KA-owned!
284
+ if (!INTERNALLY_HOSTED_URL_RE.test(url)) {
285
+ this.setState({
286
+ backgroundImageError:
287
+ "Images must be from sites hosted by Khan Academy. " +
288
+ "Please input a Khan Academy-owned address, or use the " +
289
+ "Add Image tool to rehost an existing image",
290
+ });
291
+ return;
292
+ }
293
+ this.setState({backgroundImageError: ""});
294
+
295
+ // We update our background image prop after the image loads below. To
296
+ // avoid weirdness when we change to a very slow URL, then a much
297
+ // faster URL, we keep track of the URL we're trying to change to.
298
+ this._leadingUrl = url;
299
+
300
+ if (!url) {
301
+ this.setUrl(url, 0, 0, silent);
302
+ return;
303
+ }
304
+
305
+ Util.getImageSize(url, (width, height) => {
306
+ if (this._leadingUrl !== url) {
307
+ return;
308
+ }
309
+
310
+ this.setUrl(url, width, height, true);
311
+ });
312
+ },
313
+
314
+ onRangeChange: function (type, newRange) {
315
+ const range = this.props.range.slice();
316
+ range[type] = newRange;
317
+ this.props.onChange({range: range});
318
+ },
319
+
320
+ serialize() {
321
+ // $FlowFixMe[object-this-reference]
322
+ return EditorJsonify.serialize.call(this);
323
+ },
324
+ });
325
+
326
+ export default ImageEditor;
@@ -0,0 +1,269 @@
1
+ /* eslint-disable react/sort-comp */
2
+ // @flow
3
+ import {components, Util} from "@khanacademy/perseus";
4
+ import PropTypes from "prop-types";
5
+ import * as React from "react";
6
+ import ReactDOM from "react-dom";
7
+ import _ from "underscore";
8
+
9
+ import BlurInput from "../components/blur-input.jsx";
10
+
11
+ const {InfoTip} = components;
12
+
13
+ const answerTypes = {
14
+ number: {
15
+ name: "Numbers",
16
+ forms: "integer, decimal, proper, improper, mixed",
17
+ },
18
+ decimal: {
19
+ name: "Decimals",
20
+ forms: "decimal",
21
+ },
22
+ integer: {
23
+ name: "Integers",
24
+ forms: "integer",
25
+ },
26
+ rational: {
27
+ name: "Fractions and mixed numbers",
28
+ forms: "integer, proper, improper, mixed",
29
+ },
30
+ improper: {
31
+ name: "Improper numbers (no mixed)",
32
+ forms: "integer, proper, improper",
33
+ },
34
+ mixed: {
35
+ name: "Mixed numbers (no improper)",
36
+ forms: "integer, proper, mixed",
37
+ },
38
+ percent: {
39
+ name: "Numbers or percents",
40
+ forms: "integer, decimal, proper, improper, mixed, percent",
41
+ },
42
+ pi: {
43
+ name: "Numbers with pi",
44
+ forms: "pi",
45
+ },
46
+ };
47
+
48
+ type Props = $FlowFixMe;
49
+
50
+ class InputNumberEditor extends React.Component<Props> {
51
+ static propTypes = {
52
+ value: PropTypes.number,
53
+ simplify: PropTypes.oneOf(["required", "optional", "enforced"]),
54
+ size: PropTypes.oneOf(["normal", "small"]),
55
+ inexact: PropTypes.bool,
56
+ maxError: PropTypes.number,
57
+ answerType: PropTypes.string,
58
+ rightAlign: PropTypes.bool,
59
+ };
60
+
61
+ static widgetName: "input-number" = "input-number";
62
+
63
+ static defaultProps: Props = {
64
+ value: 0,
65
+ simplify: "required",
66
+ size: "normal",
67
+ inexact: false,
68
+ maxError: 0.1,
69
+ answerType: "number",
70
+ rightAlign: false,
71
+ };
72
+
73
+ handleAnswerChange: (string) => void = (str) => {
74
+ const value = Util.firstNumericalParse(str) || 0;
75
+ this.props.onChange({value: value});
76
+ };
77
+
78
+ render(): React.Node {
79
+ const answerTypeOptions = _.map(
80
+ answerTypes,
81
+ function (v, k) {
82
+ return (
83
+ <option value={k} key={k}>
84
+ {v.name}
85
+ </option>
86
+ );
87
+ },
88
+ this,
89
+ );
90
+
91
+ return (
92
+ <div>
93
+ <div>
94
+ <label>
95
+ Correct answer:{" "}
96
+ <BlurInput
97
+ value={"" + this.props.value}
98
+ onChange={this.handleAnswerChange}
99
+ // eslint-disable-next-line react/no-string-refs
100
+ ref="input"
101
+ />
102
+ </label>
103
+ </div>
104
+
105
+ <div>
106
+ <label>
107
+ Unsimplified answers{" "}
108
+ <select
109
+ value={this.props.simplify}
110
+ onChange={(e) => {
111
+ this.props.onChange({
112
+ simplify: e.target.value,
113
+ });
114
+ }}
115
+ >
116
+ <option value="required">will not be graded</option>
117
+ <option value="optional">will be accepted</option>
118
+ <option value="enforced">
119
+ will be marked wrong
120
+ </option>
121
+ </select>
122
+ </label>
123
+ <InfoTip>
124
+ <p>
125
+ Normally select "will not be graded". This will give
126
+ the user a message saying the answer is correct but
127
+ not simplified. The user will then have to simplify
128
+ it and re-enter, but will not be penalized. (5th
129
+ grade and anything after)
130
+ </p>
131
+ <p>
132
+ Select "will be accepted" only if the user is not
133
+ expected to know how to simplify fractions yet.
134
+ (Anything prior to 5th grade)
135
+ </p>
136
+ <p>
137
+ Select "will be marked wrong" only if we are
138
+ specifically assessing the ability to simplify.
139
+ </p>
140
+ </InfoTip>
141
+ </div>
142
+
143
+ <div>
144
+ <label>
145
+ <input
146
+ type="checkbox"
147
+ checked={this.props.inexact}
148
+ onChange={(e) => {
149
+ this.props.onChange({
150
+ inexact: e.target.checked,
151
+ });
152
+ }}
153
+ />{" "}
154
+ Allow inexact answers
155
+ </label>
156
+
157
+ <label>
158
+ <input /* TODO(emily): don't use a hidden checkbox
159
+ for alignment */
160
+ type="checkbox"
161
+ style={{visibility: "hidden"}}
162
+ />
163
+ Max error:{" "}
164
+ <input
165
+ type="text"
166
+ disabled={!this.props.inexact}
167
+ defaultValue={this.props.maxError}
168
+ onBlur={(e) => {
169
+ const ans =
170
+ "" +
171
+ // $FlowFixMe[incompatible-type]
172
+ (Util.firstNumericalParse(e.target.value) ||
173
+ 0);
174
+ e.target.value = ans;
175
+ this.props.onChange({maxError: ans});
176
+ }}
177
+ />
178
+ </label>
179
+ </div>
180
+
181
+ <div>
182
+ Answer type:{" "}
183
+ <select
184
+ value={this.props.answerType}
185
+ onChange={(e) => {
186
+ this.props.onChange({answerType: e.target.value});
187
+ }}
188
+ >
189
+ {answerTypeOptions}
190
+ </select>
191
+ <InfoTip>
192
+ <p>
193
+ Use the default "Numbers" unless the answer must be
194
+ in a specific form (e.g., question is about
195
+ converting decimals to fractions).
196
+ </p>
197
+ </InfoTip>
198
+ </div>
199
+
200
+ <div>
201
+ <label>
202
+ Width{" "}
203
+ <select
204
+ value={this.props.size}
205
+ onChange={(e) => {
206
+ this.props.onChange({size: e.target.value});
207
+ }}
208
+ >
209
+ <option value="normal">Normal (80px)</option>
210
+ <option value="small">Small (40px)</option>
211
+ </select>
212
+ </label>
213
+ <InfoTip>
214
+ <p>
215
+ Use size "Normal" for all text boxes, unless there
216
+ are multiple text boxes in one line and the answer
217
+ area is too narrow to fit them.
218
+ </p>
219
+ </InfoTip>
220
+ </div>
221
+
222
+ <div>
223
+ <label>
224
+ <input
225
+ type="checkbox"
226
+ checked={this.props.rightAlign}
227
+ onChange={(e) => {
228
+ this.props.onChange({
229
+ rightAlign: e.target.checked,
230
+ });
231
+ }}
232
+ />{" "}
233
+ Right alignment
234
+ </label>
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ focus: () => boolean = () => {
241
+ // $FlowFixMe[incompatible-use]
242
+ // $FlowFixMe[prop-missing]
243
+ ReactDOM.findDOMNode(this.refs.input).focus(); // eslint-disable-line react/no-string-refs
244
+ return true;
245
+ };
246
+
247
+ serialize: () => {|
248
+ value: $FlowFixMe,
249
+ simplify: $FlowFixMe,
250
+ size: $FlowFixMe,
251
+ inexact: $FlowFixMe,
252
+ maxError: $FlowFixMe,
253
+ answerType: $FlowFixMe,
254
+ rightAlign: $FlowFixMe,
255
+ |} = () => {
256
+ return _.pick(
257
+ this.props,
258
+ "value",
259
+ "simplify",
260
+ "size",
261
+ "inexact",
262
+ "maxError",
263
+ "answerType",
264
+ "rightAlign",
265
+ );
266
+ };
267
+ }
268
+
269
+ export default InputNumberEditor;