@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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/es/index.js +10 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +6 -1
- package/package.json +16 -4
- package/src/README.md +7 -0
- package/src/__stories__/editor.stories.js +33 -0
- package/src/__stories__/section-control-button.stories.jsx +27 -0
- package/src/__testdata__/input-number_testdata.js +28 -0
- package/src/__tests__/editor_test.jsx +123 -0
- package/src/__tests__/i18n_test.jsx +585 -0
- package/src/__tests__/traversal_test.jsx +378 -0
- package/src/all-editors.js +85 -0
- package/src/all-editors_test.js +17 -0
- package/src/all-widgets.js +10 -0
- package/src/article-editor.jsx +437 -0
- package/src/components/__stories__/blur-input.stories.jsx +24 -0
- package/src/components/blur-input.jsx +62 -0
- package/src/components/checkbox.jsx +194 -0
- package/src/components/device-framer.jsx +91 -0
- package/src/components/drag-target.jsx +88 -0
- package/src/components/dropdown-option.jsx +318 -0
- package/src/components/form-wrapped-text-field.jsx +248 -0
- package/src/components/graph-settings.jsx +631 -0
- package/src/components/hover-behavior.jsx +178 -0
- package/src/components/json-editor.jsx +118 -0
- package/src/components/link.jsx +173 -0
- package/src/components/simple-button.jsx +86 -0
- package/src/components/sortable.jsx +186 -0
- package/src/components/util.js +39 -0
- package/src/components/viewport-resizer.jsx +73 -0
- package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
- package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
- package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
- package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
- package/src/diffs/__tests__/split-diff_test.js +34 -0
- package/src/diffs/__tests__/string-array-diff_test.js +74 -0
- package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
- package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
- package/src/diffs/article-diff.jsx +70 -0
- package/src/diffs/item-diff.jsx +77 -0
- package/src/diffs/renderer-diff.jsx +143 -0
- package/src/diffs/split-diff.jsx +30 -0
- package/src/diffs/string-array-diff.jsx +74 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +193 -0
- package/src/diffs/widget-diff-performer.jsx +74 -0
- package/src/diffs/widget-diff.jsx +291 -0
- package/src/editor-page.jsx +351 -0
- package/src/editor.jsx +1364 -0
- package/src/hint-editor.jsx +447 -0
- package/src/i18n.jsx +139 -0
- package/src/iframe-content-renderer.jsx +201 -0
- package/src/index.js +27 -8
- package/src/item-editor.jsx +159 -0
- package/src/item-extras-editor.jsx +150 -0
- package/src/katex-error-view.jsx +87 -0
- package/src/multirenderer-editor.jsx +1073 -0
- package/src/section-control-button.jsx +53 -0
- package/src/stateful-article-editor.jsx +176 -0
- package/src/stateful-editor-page.jsx +90 -0
- package/src/styles/perseus-editor.less +1339 -0
- package/src/testing-widgets.js +12 -0
- package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
- package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
- package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
- package/src/widgets/categorizer-editor.jsx +103 -0
- package/src/widgets/cs-program-editor.jsx +258 -0
- package/src/widgets/definition-editor.jsx +76 -0
- package/src/widgets/dropdown-editor.jsx +201 -0
- package/src/widgets/example-graphie-widget-editor.jsx +70 -0
- package/src/widgets/example-graphie-widget.jsx +175 -0
- package/src/widgets/example-widget-editor.jsx +60 -0
- package/src/widgets/example-widget.jsx +153 -0
- package/src/widgets/explanation-editor.jsx +89 -0
- package/src/widgets/expression-editor.jsx +559 -0
- package/src/widgets/graded-group-editor.jsx +162 -0
- package/src/widgets/graded-group-set-editor.jsx +101 -0
- package/src/widgets/grapher-editor.jsx +154 -0
- package/src/widgets/group-editor.jsx +83 -0
- package/src/widgets/iframe-editor.jsx +200 -0
- package/src/widgets/image-editor.jsx +326 -0
- package/src/widgets/input-number-editor.jsx +269 -0
- package/src/widgets/interaction/constraint-editor.jsx +147 -0
- package/src/widgets/interaction-editor.jsx +1388 -0
- package/src/widgets/interactive-graph-editor.jsx +320 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
- package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
- package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
- package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
- package/src/widgets/label-image/answer-choices.jsx +231 -0
- package/src/widgets/label-image/behavior.jsx +92 -0
- package/src/widgets/label-image/marker.jsx +292 -0
- package/src/widgets/label-image/question-markers.jsx +181 -0
- package/src/widgets/label-image/select-image.jsx +86 -0
- package/src/widgets/label-image-editor.jsx +256 -0
- package/src/widgets/lights-puzzle-editor.jsx +267 -0
- package/src/widgets/matcher-editor.jsx +147 -0
- package/src/widgets/matrix-editor.jsx +155 -0
- package/src/widgets/measurer-editor.jsx +245 -0
- package/src/widgets/molecule-editor.jsx +63 -0
- package/src/widgets/number-line-editor.jsx +498 -0
- package/src/widgets/numeric-input-editor.jsx +554 -0
- package/src/widgets/orderer-editor.jsx +166 -0
- package/src/widgets/passage-editor.jsx +115 -0
- package/src/widgets/passage-ref-editor.jsx +77 -0
- package/src/widgets/passage-ref-target-editor.jsx +47 -0
- package/src/widgets/plotter-editor.jsx +537 -0
- package/src/widgets/radio/editor.jsx +399 -0
- package/src/widgets/reaction-diagram-editor.jsx +126 -0
- package/src/widgets/sequence-editor.jsx +205 -0
- package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
- package/src/widgets/simple-markdown-tester.jsx +91 -0
- package/src/widgets/simulator-editor.jsx +144 -0
- package/src/widgets/sorter-editor.jsx +100 -0
- package/src/widgets/table-editor.jsx +164 -0
- package/src/widgets/transformer-editor.jsx +1006 -0
- package/src/widgets/unit-editor.jsx +293 -0
- package/src/widgets/video-editor.jsx +74 -0
- package/src/__tests__/index.test.js +0 -15
|
@@ -0,0 +1,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;
|