@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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/es/index.css +1020 -0
- package/dist/es/index.js +9 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +1020 -0
- package/dist/index.js +9 -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 +121 -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 +75 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +194 -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 +1074 -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 +75 -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 +560 -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 +1389 -0
- package/src/widgets/interactive-graph-editor.jsx +322 -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 +257 -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 +398 -0
- package/src/widgets/reaction-diagram-editor.jsx +127 -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,201 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {components, icons, EditorJsonify} from "@khanacademy/perseus";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import ReactDOM from "react-dom";
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
const {InfoTip, InlineIcon} = components;
|
|
9
|
+
const {iconPlus, iconTrash} = icons;
|
|
10
|
+
|
|
11
|
+
type Props = $FlowFixMe;
|
|
12
|
+
|
|
13
|
+
class DropdownEditor extends React.Component<Props> {
|
|
14
|
+
static propTypes = {
|
|
15
|
+
choices: PropTypes.arrayOf(
|
|
16
|
+
PropTypes.shape({
|
|
17
|
+
content: PropTypes.string,
|
|
18
|
+
correct: PropTypes.bool,
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
placeholder: PropTypes.string,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static widgetName: string = "dropdown";
|
|
25
|
+
|
|
26
|
+
static defaultProps: Props = {
|
|
27
|
+
placeholder: "",
|
|
28
|
+
choices: [
|
|
29
|
+
{
|
|
30
|
+
content: "",
|
|
31
|
+
correct: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
onPlaceholderChange: (SyntheticInputEvent<>) => void = (e) => {
|
|
37
|
+
const placeholder = e.target.value;
|
|
38
|
+
this.props.onChange({placeholder: placeholder});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
onCorrectChange: (number, SyntheticInputEvent<>) => void = (
|
|
42
|
+
choiceIndex,
|
|
43
|
+
) => {
|
|
44
|
+
const choices = _.map(this.props.choices, function (choice, i) {
|
|
45
|
+
return _.extend({}, choice, {
|
|
46
|
+
correct: i === choiceIndex,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
this.props.onChange({choices: choices});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
onContentChange: (number, SyntheticInputEvent<>) => void = (
|
|
53
|
+
choiceIndex,
|
|
54
|
+
e,
|
|
55
|
+
) => {
|
|
56
|
+
const choices = this.props.choices.slice();
|
|
57
|
+
const choice = _.clone(choices[choiceIndex]);
|
|
58
|
+
choice.content = e.target.value;
|
|
59
|
+
choices[choiceIndex] = choice;
|
|
60
|
+
this.props.onChange({choices: choices});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
addChoice: (SyntheticMouseEvent<>) => void = (e) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
|
|
66
|
+
const choices = this.props.choices;
|
|
67
|
+
const blankChoice = {content: "", correct: false};
|
|
68
|
+
this.props.onChange(
|
|
69
|
+
{
|
|
70
|
+
choices: choices.concat([blankChoice]),
|
|
71
|
+
},
|
|
72
|
+
this.focus.bind(this, choices.length),
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
removeChoice: (number, SyntheticMouseEvent<>) => void = (
|
|
77
|
+
choiceIndex,
|
|
78
|
+
e,
|
|
79
|
+
) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
const choices = _(this.props.choices).clone();
|
|
82
|
+
choices.splice(choiceIndex, 1);
|
|
83
|
+
this.props.onChange({
|
|
84
|
+
choices: choices,
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
focus: (number) => boolean = (i) => {
|
|
89
|
+
// $FlowFixMe[incompatible-use]
|
|
90
|
+
// $FlowFixMe[prop-missing]
|
|
91
|
+
ReactDOM.findDOMNode(this.refs["editor" + i]).focus(); // eslint-disable-line react/no-string-refs
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
serialize: () => $FlowFixMe = () => {
|
|
96
|
+
return EditorJsonify.serialize.call(this);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
render(): React.Node {
|
|
100
|
+
const dropdownGroupName = _.uniqueId("perseus_dropdown_");
|
|
101
|
+
return (
|
|
102
|
+
<div className="perseus-widget-dropdown">
|
|
103
|
+
<div className="dropdown-info">
|
|
104
|
+
Dropdown
|
|
105
|
+
<InfoTip>
|
|
106
|
+
<p>
|
|
107
|
+
The drop down is useful for making inequalities in a
|
|
108
|
+
custom format. We normally use the symbols {"<"},{" "}
|
|
109
|
+
{">"}, ≤, ≥ (in that order) which you can copy into
|
|
110
|
+
the choices. When possible, use the "multiple
|
|
111
|
+
choice" answer type instead.
|
|
112
|
+
</p>
|
|
113
|
+
</InfoTip>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="dropdown-placeholder">
|
|
116
|
+
<input
|
|
117
|
+
type="text"
|
|
118
|
+
placeholder="Placeholder value"
|
|
119
|
+
value={this.props.placeholder}
|
|
120
|
+
onChange={this.onPlaceholderChange}
|
|
121
|
+
/>
|
|
122
|
+
<InfoTip>
|
|
123
|
+
<p>
|
|
124
|
+
This value will appear as the drop down default. It
|
|
125
|
+
should give the user some indication of the values
|
|
126
|
+
available in the drop down itself, e.g.,
|
|
127
|
+
Yes/No/Maybe.
|
|
128
|
+
</p>
|
|
129
|
+
</InfoTip>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="clearfix" />
|
|
132
|
+
<ul className="dropdown-choices">
|
|
133
|
+
{this.props.choices.map(function (choice, i) {
|
|
134
|
+
const checkedClass = choice.correct
|
|
135
|
+
? "correct"
|
|
136
|
+
: "incorrect";
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<li key={"" + i}>
|
|
140
|
+
<div>
|
|
141
|
+
<input
|
|
142
|
+
ref={"radio" + i}
|
|
143
|
+
type="radio"
|
|
144
|
+
name={dropdownGroupName}
|
|
145
|
+
checked={
|
|
146
|
+
choice.correct ? "checked" : ""
|
|
147
|
+
}
|
|
148
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
149
|
+
onChange={this.onCorrectChange.bind(
|
|
150
|
+
this,
|
|
151
|
+
i,
|
|
152
|
+
)}
|
|
153
|
+
value={i}
|
|
154
|
+
/>
|
|
155
|
+
<input
|
|
156
|
+
type="text"
|
|
157
|
+
ref={"editor" + i}
|
|
158
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
159
|
+
onChange={this.onContentChange.bind(
|
|
160
|
+
this,
|
|
161
|
+
i,
|
|
162
|
+
)}
|
|
163
|
+
className={checkedClass}
|
|
164
|
+
value={choice.content}
|
|
165
|
+
/>
|
|
166
|
+
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
167
|
+
<a
|
|
168
|
+
href="#"
|
|
169
|
+
className="simple-button orange"
|
|
170
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
171
|
+
onClick={this.removeChoice.bind(
|
|
172
|
+
this,
|
|
173
|
+
i,
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<span className="remove-choice">
|
|
177
|
+
<InlineIcon {...iconTrash} />
|
|
178
|
+
</span>
|
|
179
|
+
</a>
|
|
180
|
+
</div>
|
|
181
|
+
</li>
|
|
182
|
+
);
|
|
183
|
+
}, this)}
|
|
184
|
+
</ul>
|
|
185
|
+
|
|
186
|
+
<div className="add-choice-container">
|
|
187
|
+
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
188
|
+
<a
|
|
189
|
+
href="#"
|
|
190
|
+
className="simple-button orange"
|
|
191
|
+
onClick={this.addChoice}
|
|
192
|
+
>
|
|
193
|
+
<InlineIcon {...iconPlus} /> Add a choice{" "}
|
|
194
|
+
</a>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default DropdownEditor;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {Changeable, EditorJsonify} from "@khanacademy/perseus";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import ExampleGraphie from "./example-graphie-widget.jsx";
|
|
6
|
+
|
|
7
|
+
const ExampleGraphieWidget = ExampleGraphie.widget;
|
|
8
|
+
|
|
9
|
+
type Props = $FlowFixMe;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This is the widget's editor. This is what shows up on the left side
|
|
13
|
+
* of the screen in the demo page. Only the question writer sees this.
|
|
14
|
+
*/
|
|
15
|
+
class ExampleGraphieWidgetEditor extends React.Component<Props> {
|
|
16
|
+
static propTypes = {
|
|
17
|
+
...Changeable.propTypes,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static widgetName: "example-graphie-widget" = "example-graphie-widget";
|
|
21
|
+
|
|
22
|
+
static defaultProps: Props = {
|
|
23
|
+
correct: [4, 4],
|
|
24
|
+
graph: {
|
|
25
|
+
box: [340, 340],
|
|
26
|
+
labels: ["x", "y"],
|
|
27
|
+
range: [
|
|
28
|
+
[-10, 10],
|
|
29
|
+
[-10, 10],
|
|
30
|
+
],
|
|
31
|
+
step: [1, 1],
|
|
32
|
+
gridStep: [1, 1],
|
|
33
|
+
valid: true,
|
|
34
|
+
backgroundImage: null,
|
|
35
|
+
markings: "grid",
|
|
36
|
+
showProtractor: false,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
41
|
+
return Changeable.change.apply(this, args);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
handleChange: ($FlowFixMe) => void = (newProps) => {
|
|
45
|
+
if (newProps.coord) {
|
|
46
|
+
this.change({
|
|
47
|
+
correct: newProps.coord,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
serialize: () => $FlowFixMe = () => {
|
|
53
|
+
return EditorJsonify.serialize.call(this);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
render(): React.Node {
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<ExampleGraphieWidget
|
|
60
|
+
graph={this.props.graph}
|
|
61
|
+
coord={this.props.correct}
|
|
62
|
+
onChange={this.handleChange}
|
|
63
|
+
apiOptions={this.props.apiOptions}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default ExampleGraphieWidgetEditor;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars, react/forbid-prop-types, react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
/**
|
|
4
|
+
* This is an example graphie-using widget
|
|
5
|
+
*
|
|
6
|
+
* TODO(jack): Add more comments
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {number as knumber, point as kpoint} from "@khanacademy/kmath";
|
|
10
|
+
import {
|
|
11
|
+
components,
|
|
12
|
+
ApiOptions,
|
|
13
|
+
Changeable,
|
|
14
|
+
Util,
|
|
15
|
+
WidgetJsonifyDeprecated,
|
|
16
|
+
} from "@khanacademy/perseus";
|
|
17
|
+
import PropTypes from "prop-types";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import _ from "underscore";
|
|
20
|
+
|
|
21
|
+
import type {Coord, WidgetExports} from "@khanacademy/perseus";
|
|
22
|
+
|
|
23
|
+
const {Graphie} = components;
|
|
24
|
+
|
|
25
|
+
// $FlowFixMe[prop-missing]
|
|
26
|
+
const MovablePoint = Graphie.MovablePoint;
|
|
27
|
+
|
|
28
|
+
type Props = $FlowFixMe;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This is the widget's renderer. It shows up in the right column
|
|
32
|
+
* in the demo, and is what is visible to users, and where
|
|
33
|
+
* users enter their answers.
|
|
34
|
+
*/
|
|
35
|
+
class ExampleGraphieWidget extends React.Component<Props> {
|
|
36
|
+
static propTypes = {
|
|
37
|
+
...Changeable.propTypes,
|
|
38
|
+
apiOptions: ApiOptions.propTypes,
|
|
39
|
+
|
|
40
|
+
graph: PropTypes.object.isRequired,
|
|
41
|
+
coord: PropTypes.arrayOf(PropTypes.number),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
static defaultProps: Props = {
|
|
45
|
+
// We want to allow our coord to be null to test if the
|
|
46
|
+
// user has interacted with this widget yet when grading it
|
|
47
|
+
coord: null,
|
|
48
|
+
graph: {
|
|
49
|
+
box: [400, 400],
|
|
50
|
+
labels: ["x", "y"],
|
|
51
|
+
range: [
|
|
52
|
+
[-10, 10],
|
|
53
|
+
[-10, 10],
|
|
54
|
+
],
|
|
55
|
+
step: [1, 1],
|
|
56
|
+
gridStep: [1, 1],
|
|
57
|
+
valid: true,
|
|
58
|
+
backgroundImage: null,
|
|
59
|
+
markings: "grid",
|
|
60
|
+
showProtractor: false,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This is the widget's grading function
|
|
66
|
+
*/
|
|
67
|
+
static validate(state: $FlowFixMe, rubric: $FlowFixMe): $FlowFixMe {
|
|
68
|
+
if (state.coord == null) {
|
|
69
|
+
return {
|
|
70
|
+
type: "invalid",
|
|
71
|
+
message: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (kpoint.equal(state.coord, rubric.correct)) {
|
|
75
|
+
return {
|
|
76
|
+
type: "points",
|
|
77
|
+
earned: 1,
|
|
78
|
+
total: 1,
|
|
79
|
+
message: null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
type: "points",
|
|
84
|
+
earned: 0,
|
|
85
|
+
total: 1,
|
|
86
|
+
message: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getUserInput: () => $FlowFixMe = () => {
|
|
91
|
+
return WidgetJsonifyDeprecated.getUserInput.call(this);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
render(): React.Node {
|
|
95
|
+
return (
|
|
96
|
+
<Graphie
|
|
97
|
+
// eslint-disable-next-line react/no-string-refs
|
|
98
|
+
ref="graphie"
|
|
99
|
+
box={this.props.graph.box}
|
|
100
|
+
range={this.props.graph.range}
|
|
101
|
+
options={this.props.graph}
|
|
102
|
+
setup={this.setupGraphie}
|
|
103
|
+
setDrawingAreaAvailable={
|
|
104
|
+
this.props.apiOptions.setDrawingAreaAvailable
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
<MovablePoint
|
|
108
|
+
pointSize={5}
|
|
109
|
+
coord={this.props.coord || [0, 0]}
|
|
110
|
+
constraints={[
|
|
111
|
+
MovablePoint.constraints.snap(),
|
|
112
|
+
MovablePoint.constraints.bound(),
|
|
113
|
+
]}
|
|
114
|
+
onMove={this.movePoint}
|
|
115
|
+
/>
|
|
116
|
+
</Graphie>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
121
|
+
return Changeable.change.apply(this, args);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
movePoint: (Coord) => void = (newCoord) => {
|
|
125
|
+
this.change({
|
|
126
|
+
coord: newCoord,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
_getGridConfig: ($FlowFixMe) => $FlowFixMe = (options) => {
|
|
131
|
+
return _.map(options.step, function (step, i) {
|
|
132
|
+
return Util.gridDimensionConfig(
|
|
133
|
+
step,
|
|
134
|
+
options.range[i],
|
|
135
|
+
options.box[i],
|
|
136
|
+
options.gridStep[i],
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
setupGraphie: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
|
|
142
|
+
graphie,
|
|
143
|
+
options,
|
|
144
|
+
) => {
|
|
145
|
+
const gridConfig = this._getGridConfig(options);
|
|
146
|
+
graphie.graphInit({
|
|
147
|
+
range: options.range,
|
|
148
|
+
scale: _.pluck(gridConfig, "scale"),
|
|
149
|
+
axisArrows: "<->",
|
|
150
|
+
labelFormat: function (s) {
|
|
151
|
+
return "\\small{" + s + "}";
|
|
152
|
+
},
|
|
153
|
+
gridStep: options.gridStep,
|
|
154
|
+
tickStep: _.pluck(gridConfig, "tickStep"),
|
|
155
|
+
labelStep: 1,
|
|
156
|
+
unityLabels: _.pluck(gridConfig, "unityLabel"),
|
|
157
|
+
});
|
|
158
|
+
graphie.label([0, options.range[1][1]], options.labels[1], "above");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
simpleValidate: ($FlowFixMe) => $FlowFixMe = (rubric) => {
|
|
162
|
+
return ExampleGraphieWidget.validate(this.getUserInput(), rubric);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* For this widget to work, we must export it.
|
|
168
|
+
* We also must import this file in src/all-widgets.js
|
|
169
|
+
*/
|
|
170
|
+
export default ({
|
|
171
|
+
name: "example-graphie-widget",
|
|
172
|
+
displayName: "Example Graphie Widget",
|
|
173
|
+
hidden: true, // Hides this widget from the Perseus.Editor widget select
|
|
174
|
+
widget: ExampleGraphieWidget,
|
|
175
|
+
}: WidgetExports<typeof ExampleGraphieWidget>);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* eslint-disable react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {Changeable, EditorJsonify} from "@khanacademy/perseus";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
type Props = $FlowFixMe;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This is the widget's editor. This is what shows up on the left side
|
|
10
|
+
* of the screen in the demo. Only the question writer sees this.
|
|
11
|
+
*/
|
|
12
|
+
class ExampleWidgetEditor extends React.Component<Props> {
|
|
13
|
+
static propTypes = {
|
|
14
|
+
...Changeable.propTypes,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
static widgetName: "example-widget" = "example-widget";
|
|
18
|
+
|
|
19
|
+
static defaultProps: Props = {
|
|
20
|
+
correct: "",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
handleAnswerChange: (SyntheticInputEvent<>) => void = (event) => {
|
|
24
|
+
this.change({
|
|
25
|
+
correct: event.target.value,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
render(): React.Node {
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<label>
|
|
33
|
+
Correct answer:
|
|
34
|
+
<input
|
|
35
|
+
value={this.props.correct}
|
|
36
|
+
onChange={this.handleAnswerChange}
|
|
37
|
+
// eslint-disable-next-line react/no-string-refs
|
|
38
|
+
ref="input"
|
|
39
|
+
/>
|
|
40
|
+
</label>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
46
|
+
return Changeable.change.apply(this, args);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
focus: () => boolean = () => {
|
|
50
|
+
// eslint-disable-next-line react/no-string-refs
|
|
51
|
+
this.refs.input.focus();
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
serialize: () => $FlowFixMe = () => {
|
|
56
|
+
return EditorJsonify.serialize.call(this);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default ExampleWidgetEditor;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* eslint-disable react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
/**
|
|
4
|
+
* This is a simple number-entry widget
|
|
5
|
+
* It is not as powerful as number-input, but has a simpler, more
|
|
6
|
+
* representative structure as an example widget, and is easier to
|
|
7
|
+
* test new ideas on.
|
|
8
|
+
*
|
|
9
|
+
* TODO(jack): Add more comments
|
|
10
|
+
*/
|
|
11
|
+
import {Changeable} from "@khanacademy/perseus";
|
|
12
|
+
import PropTypes from "prop-types";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import _ from "underscore";
|
|
15
|
+
|
|
16
|
+
import type {WidgetExports} from "@khanacademy/perseus";
|
|
17
|
+
|
|
18
|
+
class TextInput extends React.Component<$FlowFixMe> {
|
|
19
|
+
render(): React.Node {
|
|
20
|
+
return (
|
|
21
|
+
<input
|
|
22
|
+
// eslint-disable-next-line react/no-string-refs
|
|
23
|
+
ref="input"
|
|
24
|
+
value={this.props.value || ""}
|
|
25
|
+
onChange={this.changeValue}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
focus = () => {
|
|
31
|
+
// eslint-disable-next-line react/no-string-refs
|
|
32
|
+
this.refs.input.focus();
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
changeValue = (e) => {
|
|
37
|
+
// Translating from the js event e to the value
|
|
38
|
+
// of the textbox to send to onChange
|
|
39
|
+
this.props.onChange(e.target.value);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This is the widget's renderer. It shows up in the right column
|
|
45
|
+
* in the demo, and is what is visible to users, and where
|
|
46
|
+
* users enter their answers.
|
|
47
|
+
*/
|
|
48
|
+
class ExampleWidget extends React.Component<$FlowFixMe> {
|
|
49
|
+
static propTypes = {
|
|
50
|
+
...Changeable.propTypes,
|
|
51
|
+
value: PropTypes.string,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
static defaultProps: $FlowFixMe = {
|
|
55
|
+
value: "",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This is the widget's grading function. simpleValidate generally
|
|
60
|
+
* defers to this function.
|
|
61
|
+
*
|
|
62
|
+
* value is usually the result of getUserInput on the widget
|
|
63
|
+
* rubric is the result of calling serialize() on the editor
|
|
64
|
+
*/
|
|
65
|
+
static validate(value: string, rubric: $FlowFixMe): $FlowFixMe {
|
|
66
|
+
if (value === "") {
|
|
67
|
+
return {
|
|
68
|
+
type: "invalid",
|
|
69
|
+
message:
|
|
70
|
+
"It looks like you haven't answered all of the " +
|
|
71
|
+
"question yet.",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (value === rubric.correct) {
|
|
75
|
+
return {
|
|
76
|
+
type: "points",
|
|
77
|
+
earned: 1,
|
|
78
|
+
total: 1,
|
|
79
|
+
message: null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
type: "points",
|
|
84
|
+
earned: 0,
|
|
85
|
+
total: 1,
|
|
86
|
+
message: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tell our parent to update our props.
|
|
92
|
+
*/
|
|
93
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
94
|
+
return Changeable.change.apply(this, args);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
render(): React.Node {
|
|
98
|
+
return (
|
|
99
|
+
<TextInput
|
|
100
|
+
// eslint-disable-next-line react/no-string-refs
|
|
101
|
+
ref="input"
|
|
102
|
+
value={this.props.value}
|
|
103
|
+
onChange={this.change("value")}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getUserInput: () => string = () => {
|
|
109
|
+
return this.props.value;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Widgets that are focusable should add a focus method that returns
|
|
114
|
+
* true if focusing succeeded. The first such widget found will be
|
|
115
|
+
* focused on page load.
|
|
116
|
+
*/
|
|
117
|
+
focus: () => boolean = () => {
|
|
118
|
+
// eslint-disable-next-line react/no-string-refs
|
|
119
|
+
this.refs.input.focus();
|
|
120
|
+
return true;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* simpleValidate is called for grading. Rubric is the result of calling
|
|
125
|
+
* getUserInput() on the editor that created this widget.
|
|
126
|
+
*
|
|
127
|
+
* Should return an object representing the grading result, such as
|
|
128
|
+
* {
|
|
129
|
+
* type: "points",
|
|
130
|
+
* earned: 1,
|
|
131
|
+
* total: 1,
|
|
132
|
+
* message: null
|
|
133
|
+
* }
|
|
134
|
+
*/
|
|
135
|
+
simpleValidate: ($FlowFixMe) => $FlowFixMe = (rubric) => {
|
|
136
|
+
return ExampleWidget.validate(this.getUserInput(), rubric);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* For this widget to work, we must import this file in src/all-widgets.js
|
|
142
|
+
*/
|
|
143
|
+
export default ({
|
|
144
|
+
name: "example-widget",
|
|
145
|
+
displayName: "Example Widget",
|
|
146
|
+
|
|
147
|
+
// Tell the renderer what type of `display:` style we would like
|
|
148
|
+
// for the component wrapping this one.
|
|
149
|
+
defaultAlignment: "inline-block",
|
|
150
|
+
|
|
151
|
+
hidden: true, // Hides this widget from the Perseus.Editor widget select
|
|
152
|
+
widget: ExampleWidget,
|
|
153
|
+
}: WidgetExports<typeof ExampleWidget>);
|