@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,162 @@
|
|
|
1
|
+
/* eslint-disable react/forbid-prop-types, react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {components, icons, ApiOptions, Changeable} from "@khanacademy/perseus";
|
|
4
|
+
import {StyleSheet, css} from "aphrodite";
|
|
5
|
+
import PropTypes from "prop-types";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import _ from "underscore";
|
|
8
|
+
|
|
9
|
+
import Editor from "../editor.jsx";
|
|
10
|
+
|
|
11
|
+
const {InlineIcon, TextInput} = components;
|
|
12
|
+
const {iconPlus, iconTrash} = icons;
|
|
13
|
+
|
|
14
|
+
type Props = $FlowFixMe;
|
|
15
|
+
|
|
16
|
+
class GradedGroupEditor extends React.Component<Props> {
|
|
17
|
+
static propTypes = {
|
|
18
|
+
...Changeable.propTypes,
|
|
19
|
+
title: PropTypes.string,
|
|
20
|
+
content: PropTypes.string,
|
|
21
|
+
widgets: PropTypes.object,
|
|
22
|
+
images: PropTypes.object,
|
|
23
|
+
apiOptions: ApiOptions.propTypes,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
static widgetName: "graded-group" = "graded-group";
|
|
27
|
+
|
|
28
|
+
static defaultProps: Props = {
|
|
29
|
+
title: "",
|
|
30
|
+
content: "",
|
|
31
|
+
widgets: {},
|
|
32
|
+
images: {},
|
|
33
|
+
hint: null,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
37
|
+
return Changeable.change.apply(this, args);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
handleAddHint: () => void = () => {
|
|
41
|
+
const hint = {content: ""};
|
|
42
|
+
this.props.onChange({hint}, () => {
|
|
43
|
+
// eslint-disable-next-line react/no-string-refs
|
|
44
|
+
this.refs["hint-editor"].focus();
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
handleRemoveHint: (SyntheticMouseEvent<>) => void = (e) => {
|
|
49
|
+
this.props.onChange({hint: null});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
render(): React.Node {
|
|
53
|
+
return (
|
|
54
|
+
<div className="perseus-group-editor">
|
|
55
|
+
<div className="perseus-widget-row">
|
|
56
|
+
<label className={css(styles.title)}>
|
|
57
|
+
Title:{" "}
|
|
58
|
+
<TextInput
|
|
59
|
+
value={this.props.title}
|
|
60
|
+
className={css(styles.input)}
|
|
61
|
+
onChange={this.change("title")}
|
|
62
|
+
/>
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
<Editor
|
|
66
|
+
// eslint-disable-next-line react/no-string-refs
|
|
67
|
+
ref="editor"
|
|
68
|
+
content={this.props.content}
|
|
69
|
+
widgets={this.props.widgets}
|
|
70
|
+
apiOptions={this.props.apiOptions}
|
|
71
|
+
images={this.props.images}
|
|
72
|
+
widgetEnabled={true}
|
|
73
|
+
immutableWidgets={false}
|
|
74
|
+
onChange={this.props.onChange}
|
|
75
|
+
warnNoPrompt={true}
|
|
76
|
+
warnNoWidgets={true}
|
|
77
|
+
/>
|
|
78
|
+
{!this.props.hint && (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
style={{marginTop: 10}}
|
|
82
|
+
className="add-hint simple-button orange"
|
|
83
|
+
onClick={this.handleAddHint}
|
|
84
|
+
>
|
|
85
|
+
<InlineIcon {...iconPlus} /> Add a hint
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
{this.props.hint && (
|
|
89
|
+
<div className="perseus-hint-editor">
|
|
90
|
+
<div className={css(styles.hintsTitle)}>Hint</div>
|
|
91
|
+
<Editor
|
|
92
|
+
// eslint-disable-next-line react/no-string-refs
|
|
93
|
+
ref="hint-editor"
|
|
94
|
+
content={
|
|
95
|
+
this.props.hint ? this.props.hint.content : ""
|
|
96
|
+
}
|
|
97
|
+
widgets={
|
|
98
|
+
this.props.hint ? this.props.hint.widgets : {}
|
|
99
|
+
}
|
|
100
|
+
apiOptions={this.props.apiOptions}
|
|
101
|
+
images={this.props.hint && this.props.hint.images}
|
|
102
|
+
widgetEnabled={true}
|
|
103
|
+
immutableWidgets={false}
|
|
104
|
+
onChange={(props) => {
|
|
105
|
+
// Copy all props over from the existing hint
|
|
106
|
+
// and then add new props.
|
|
107
|
+
this.change(
|
|
108
|
+
"hint",
|
|
109
|
+
Object.assign({}, this.props.hint, props),
|
|
110
|
+
);
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className="remove-hint simple-button orange"
|
|
116
|
+
onClick={this.handleRemoveHint}
|
|
117
|
+
>
|
|
118
|
+
<InlineIcon {...iconTrash} /> Remove this hint
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getSaveWarnings: () => $FlowFixMe = () => {
|
|
127
|
+
// eslint-disable-next-line react/no-string-refs
|
|
128
|
+
return this.refs.editor.getSaveWarnings();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
serialize: () => {|title: string, hint: ?$FlowFixMe|} = () => {
|
|
132
|
+
return {
|
|
133
|
+
title: this.props.title,
|
|
134
|
+
// eslint-disable-next-line react/no-string-refs
|
|
135
|
+
...this.refs.editor.serialize(),
|
|
136
|
+
hint:
|
|
137
|
+
// eslint-disable-next-line react/no-string-refs
|
|
138
|
+
this.refs["hint-editor"] &&
|
|
139
|
+
// eslint-disable-next-line react/no-string-refs
|
|
140
|
+
this.refs["hint-editor"].serialize(),
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const styles = StyleSheet.create({
|
|
146
|
+
title: {
|
|
147
|
+
fontSize: 18,
|
|
148
|
+
fontWeight: "bold",
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
input: {
|
|
152
|
+
fontSize: 18,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
hintsTitle: {
|
|
156
|
+
marginTop: 10,
|
|
157
|
+
fontSize: "110%",
|
|
158
|
+
fontWeight: "bold",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export default GradedGroupEditor;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* eslint-disable react/forbid-prop-types, react/no-unsafe */
|
|
2
|
+
// @flow
|
|
3
|
+
import {ApiOptions, Changeable} from "@khanacademy/perseus";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
import GradedGroupEditor from "./graded-group-editor.jsx";
|
|
9
|
+
|
|
10
|
+
type Props = $FlowFixMe;
|
|
11
|
+
|
|
12
|
+
class GradedGroupSetEditor extends React.Component<Props> {
|
|
13
|
+
// eslint-disable-next-line flowtype/no-mutable-array
|
|
14
|
+
_editors: Array<$FlowFixMe>;
|
|
15
|
+
|
|
16
|
+
static propTypes = {
|
|
17
|
+
...Changeable.propTypes,
|
|
18
|
+
apiOptions: ApiOptions.propTypes,
|
|
19
|
+
gradedGroups: PropTypes.array,
|
|
20
|
+
onChange: PropTypes.func.isRequired,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
static widgetName: "graded-group-set" = "graded-group-set";
|
|
24
|
+
|
|
25
|
+
static defaultProps: Props = {
|
|
26
|
+
gradedGroups: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
|
|
30
|
+
UNSAFE_componentWillMount() {
|
|
31
|
+
this._editors = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
35
|
+
return Changeable.change.apply(this, args);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
getSaveWarnings: () => $ReadOnlyArray<$FlowFixMe> = () => {
|
|
39
|
+
return [].concat(
|
|
40
|
+
...this._editors.map((editor) =>
|
|
41
|
+
editor ? editor.getSaveWarnings() : [],
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
serialize: () => {|gradedGroups: $FlowFixMe|} = () => {
|
|
47
|
+
return {
|
|
48
|
+
gradedGroups: this.props.gradedGroups,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
renderGroups: () => React.Node = () => {
|
|
53
|
+
if (!this.props.gradedGroups) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return this.props.gradedGroups.map((group, i) => (
|
|
57
|
+
<GradedGroupEditor
|
|
58
|
+
key={i}
|
|
59
|
+
ref={(el) => (this._editors[i] = el)}
|
|
60
|
+
{...group}
|
|
61
|
+
apiOptions={this.props.apiOptions}
|
|
62
|
+
widgetEnabled={true}
|
|
63
|
+
immutableWidgets={false}
|
|
64
|
+
onChange={(data) =>
|
|
65
|
+
this.change(
|
|
66
|
+
"gradedGroups",
|
|
67
|
+
setArrayItem(this.props.gradedGroups, i, {
|
|
68
|
+
...this.props.gradedGroups[i],
|
|
69
|
+
...data,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
addGroup: () => void = () => {
|
|
78
|
+
const groups = this.props.gradedGroups || [];
|
|
79
|
+
this.change(
|
|
80
|
+
"gradedGroups",
|
|
81
|
+
groups.concat([GradedGroupEditor.defaultProps]),
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
render(): React.Node {
|
|
86
|
+
return (
|
|
87
|
+
<div className="perseus-group-editor">
|
|
88
|
+
{this.renderGroups()}
|
|
89
|
+
<button onClick={this.addGroup}>Add group</button>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const setArrayItem = (list, i, value) => [
|
|
96
|
+
...list.slice(0, i),
|
|
97
|
+
value,
|
|
98
|
+
...list.slice(i + 1),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
export default GradedGroupSetEditor;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* eslint-disable react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {
|
|
4
|
+
components,
|
|
5
|
+
Changeable,
|
|
6
|
+
GrapherUtil,
|
|
7
|
+
GrapherWidget,
|
|
8
|
+
SizingUtils,
|
|
9
|
+
} from "@khanacademy/perseus";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import _ from "underscore";
|
|
12
|
+
|
|
13
|
+
import GraphSettings from "../components/graph-settings.jsx";
|
|
14
|
+
|
|
15
|
+
const {InfoTip, MultiButtonGroup} = components;
|
|
16
|
+
const {containerSizeClass, getInteractiveBoxFromSizeClass} = SizingUtils;
|
|
17
|
+
const Grapher = GrapherWidget.widget;
|
|
18
|
+
const {
|
|
19
|
+
DEFAULT_GRAPHER_PROPS,
|
|
20
|
+
allTypes,
|
|
21
|
+
chooseType,
|
|
22
|
+
defaultPlotProps,
|
|
23
|
+
getEquationString,
|
|
24
|
+
typeToButton,
|
|
25
|
+
} = GrapherUtil;
|
|
26
|
+
|
|
27
|
+
type Props = $FlowFixMe;
|
|
28
|
+
|
|
29
|
+
class GrapherEditor extends React.Component<Props> {
|
|
30
|
+
static propTypes = {
|
|
31
|
+
...Changeable.propTypes,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
static widgetName: "grapher" = "grapher";
|
|
35
|
+
|
|
36
|
+
static defaultProps: Props = {
|
|
37
|
+
correct: DEFAULT_GRAPHER_PROPS.plot,
|
|
38
|
+
graph: DEFAULT_GRAPHER_PROPS.graph,
|
|
39
|
+
availableTypes: DEFAULT_GRAPHER_PROPS.availableTypes,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
43
|
+
return Changeable.change.apply(this, args);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
render(): React.Node {
|
|
47
|
+
const sizeClass = containerSizeClass.SMALL;
|
|
48
|
+
let equationString;
|
|
49
|
+
let graph;
|
|
50
|
+
if (this.props.graph.valid === true) {
|
|
51
|
+
const graphProps = {
|
|
52
|
+
graph: this.props.graph,
|
|
53
|
+
plot: this.props.correct,
|
|
54
|
+
availableTypes: this.props.availableTypes,
|
|
55
|
+
onChange: (newProps, cb) => {
|
|
56
|
+
let correct = this.props.correct;
|
|
57
|
+
if (correct.type === newProps.plot?.type) {
|
|
58
|
+
correct = _.extend({}, correct, newProps.plot);
|
|
59
|
+
} else {
|
|
60
|
+
// Clear options from previous graph
|
|
61
|
+
correct = newProps.plot;
|
|
62
|
+
}
|
|
63
|
+
this.props.onChange({correct: correct}, cb);
|
|
64
|
+
},
|
|
65
|
+
trackInteraction: function () {},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
graph = (
|
|
69
|
+
// NOTE(jeremy): This editor doesn't pass in a bunch of
|
|
70
|
+
// standard props that the Renderer provides normally (eg.
|
|
71
|
+
// alignment, findWidgets, etc).
|
|
72
|
+
// $FlowFixMe[prop-missing]
|
|
73
|
+
<Grapher
|
|
74
|
+
{...graphProps}
|
|
75
|
+
apiOptions={this.props.apiOptions}
|
|
76
|
+
containerSizeClass={sizeClass}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
equationString = getEquationString(graphProps);
|
|
80
|
+
} else {
|
|
81
|
+
graph = (
|
|
82
|
+
<div className="perseus-error">{this.props.graph.valid}</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<div>
|
|
89
|
+
Correct answer{" "}
|
|
90
|
+
<InfoTip>
|
|
91
|
+
<p>
|
|
92
|
+
Graph the correct answer in the graph below and
|
|
93
|
+
ensure the equation or point coordinates displayed
|
|
94
|
+
represent the correct answer.
|
|
95
|
+
</p>
|
|
96
|
+
</InfoTip>{" "}
|
|
97
|
+
: {equationString}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<GraphSettings
|
|
101
|
+
editableSettings={["graph", "snap", "image"]}
|
|
102
|
+
box={getInteractiveBoxFromSizeClass(sizeClass)}
|
|
103
|
+
range={this.props.graph.range}
|
|
104
|
+
labels={this.props.graph.labels}
|
|
105
|
+
step={this.props.graph.step}
|
|
106
|
+
gridStep={this.props.graph.gridStep}
|
|
107
|
+
snapStep={this.props.graph.snapStep}
|
|
108
|
+
valid={this.props.graph.valid}
|
|
109
|
+
backgroundImage={this.props.graph.backgroundImage}
|
|
110
|
+
markings={this.props.graph.markings}
|
|
111
|
+
rulerLabel={this.props.graph.rulerLabel}
|
|
112
|
+
rulerTicks={this.props.graph.rulerTicks}
|
|
113
|
+
showTooltips={this.props.graph.showTooltips}
|
|
114
|
+
onChange={this.change("graph")}
|
|
115
|
+
/>
|
|
116
|
+
<div className="perseus-widget-row">
|
|
117
|
+
<label>Available functions: </label>
|
|
118
|
+
<MultiButtonGroup
|
|
119
|
+
allowEmpty={false}
|
|
120
|
+
values={this.props.availableTypes}
|
|
121
|
+
buttons={_.map(allTypes, typeToButton)}
|
|
122
|
+
onChange={this.handleAvailableTypesChange}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
{graph}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
handleAvailableTypesChange: ($FlowFixMe) => void = (newAvailableTypes) => {
|
|
131
|
+
let correct = this.props.correct;
|
|
132
|
+
|
|
133
|
+
// If the currently 'correct' type is removed from the list of types,
|
|
134
|
+
// we need to change it to avoid impossible questions.
|
|
135
|
+
if (!_.contains(newAvailableTypes, this.props.correct.type)) {
|
|
136
|
+
const graph = this.props.graph;
|
|
137
|
+
const newType = chooseType(newAvailableTypes);
|
|
138
|
+
correct = defaultPlotProps(newType, graph);
|
|
139
|
+
}
|
|
140
|
+
this.props.onChange({
|
|
141
|
+
availableTypes: newAvailableTypes,
|
|
142
|
+
correct: correct,
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
serialize: () => $FlowFixMe = () => {
|
|
147
|
+
return _.chain(this.props)
|
|
148
|
+
.pick("correct", "availableTypes")
|
|
149
|
+
.extend({graph: _.omit(this.props.graph, "box")})
|
|
150
|
+
.value();
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default GrapherEditor;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* eslint-disable react/forbid-prop-types, react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {ApiOptions, Changeable} from "@khanacademy/perseus";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
import Editor from "../editor.jsx";
|
|
9
|
+
|
|
10
|
+
type Props = $FlowFixMe;
|
|
11
|
+
|
|
12
|
+
class GroupEditor extends React.Component<Props> {
|
|
13
|
+
static propTypes = {
|
|
14
|
+
...Changeable.propTypes,
|
|
15
|
+
content: PropTypes.string,
|
|
16
|
+
widgets: PropTypes.object,
|
|
17
|
+
images: PropTypes.object,
|
|
18
|
+
metadata: PropTypes.any,
|
|
19
|
+
apiOptions: ApiOptions.propTypes,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static widgetName: "group" = "group";
|
|
23
|
+
|
|
24
|
+
static defaultProps: Props = {
|
|
25
|
+
content: "",
|
|
26
|
+
widgets: {},
|
|
27
|
+
images: {},
|
|
28
|
+
// `undefined` instead of `null` so that getDefaultProps works for
|
|
29
|
+
// `the GroupMetadataEditor`
|
|
30
|
+
metadata: undefined,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
render(): React.Node {
|
|
34
|
+
return (
|
|
35
|
+
<div className="perseus-group-editor">
|
|
36
|
+
<div>
|
|
37
|
+
{/* the metadata editor; used for tags on
|
|
38
|
+
khanacademy.org */}
|
|
39
|
+
{this._renderMetadataEditor()}
|
|
40
|
+
</div>
|
|
41
|
+
<Editor
|
|
42
|
+
// eslint-disable-next-line react/no-string-refs
|
|
43
|
+
ref="editor"
|
|
44
|
+
content={this.props.content}
|
|
45
|
+
widgets={this.props.widgets}
|
|
46
|
+
apiOptions={this.props.apiOptions}
|
|
47
|
+
images={this.props.images}
|
|
48
|
+
widgetEnabled={true}
|
|
49
|
+
immutableWidgets={false}
|
|
50
|
+
onChange={this.props.onChange}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_renderMetadataEditor: () => React.Node = () => {
|
|
57
|
+
const GroupMetadataEditor = this.props.apiOptions.GroupMetadataEditor;
|
|
58
|
+
return (
|
|
59
|
+
<GroupMetadataEditor
|
|
60
|
+
value={this.props.metadata}
|
|
61
|
+
onChange={this.change("metadata")}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
67
|
+
return Changeable.change.apply(this, args);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
getSaveWarnings: () => $ReadOnlyArray<$FlowFixMe> = () => {
|
|
71
|
+
// eslint-disable-next-line react/no-string-refs
|
|
72
|
+
return this.refs.editor.getSaveWarnings();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
serialize: () => $FlowFixMe = () => {
|
|
76
|
+
// eslint-disable-next-line react/no-string-refs
|
|
77
|
+
return _.extend({}, this.refs.editor.serialize(), {
|
|
78
|
+
metadata: this.props.metadata,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default GroupEditor;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* eslint-disable react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {components, Changeable, EditorJsonify} from "@khanacademy/perseus";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
import BlurInput from "../components/blur-input.jsx";
|
|
9
|
+
|
|
10
|
+
const {PropCheckBox} = components;
|
|
11
|
+
|
|
12
|
+
type PairEditorProps = $FlowFixMe;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* This is used for editing a name/value pair.
|
|
16
|
+
*/
|
|
17
|
+
class PairEditor extends React.Component<PairEditorProps> {
|
|
18
|
+
static propTypes = {
|
|
19
|
+
...Changeable.propTypes,
|
|
20
|
+
name: PropTypes.string,
|
|
21
|
+
value: PropTypes.string,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static defaultProps: PairEditorProps = {
|
|
25
|
+
name: "",
|
|
26
|
+
value: "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
change = (...args) => {
|
|
30
|
+
return Changeable.change.apply(this, args);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
render(): React.Node {
|
|
34
|
+
return (
|
|
35
|
+
<fieldset>
|
|
36
|
+
<label>
|
|
37
|
+
Name:
|
|
38
|
+
<BlurInput
|
|
39
|
+
value={this.props.name}
|
|
40
|
+
// $FlowFixMe[incompatible-type] single-param call returns a callback
|
|
41
|
+
onChange={this.change("name")}
|
|
42
|
+
/>
|
|
43
|
+
</label>
|
|
44
|
+
<label>
|
|
45
|
+
Value:
|
|
46
|
+
<BlurInput
|
|
47
|
+
value={this.props.value}
|
|
48
|
+
// $FlowFixMe[incompatible-type] single-param call returns a callback
|
|
49
|
+
onChange={this.change("value")}
|
|
50
|
+
/>
|
|
51
|
+
</label>
|
|
52
|
+
</fieldset>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
serialize = () => {
|
|
57
|
+
return EditorJsonify.serialize.call(this);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type PairsEditorProps = $FlowFixMe;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* This is used for editing a set of name/value pairs.
|
|
65
|
+
*/
|
|
66
|
+
class PairsEditor extends React.Component<PairsEditorProps> {
|
|
67
|
+
static propTypes = {
|
|
68
|
+
...Changeable.propTypes,
|
|
69
|
+
pairs: PropTypes.arrayOf(
|
|
70
|
+
PropTypes.shape({
|
|
71
|
+
name: PropTypes.string,
|
|
72
|
+
value: PropTypes.string,
|
|
73
|
+
}),
|
|
74
|
+
).isRequired,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
render(): React.Node {
|
|
78
|
+
const editors = _.map(this.props.pairs, (pair, i) => {
|
|
79
|
+
return (
|
|
80
|
+
<PairEditor
|
|
81
|
+
key={i}
|
|
82
|
+
name={pair.name}
|
|
83
|
+
value={pair.value}
|
|
84
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
85
|
+
onChange={this.handlePairChange.bind(this, i)}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
return <div>{editors}</div>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
change = (...args) => {
|
|
93
|
+
return Changeable.change.apply(this, args);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
handlePairChange = (pairIndex, pair) => {
|
|
97
|
+
// If they're both non empty, add a new one
|
|
98
|
+
const pairs = this.props.pairs.slice();
|
|
99
|
+
pairs[pairIndex] = pair;
|
|
100
|
+
|
|
101
|
+
const lastPair = pairs[pairs.length - 1];
|
|
102
|
+
if (lastPair.name && lastPair.value) {
|
|
103
|
+
pairs.push({name: "", value: ""});
|
|
104
|
+
}
|
|
105
|
+
this.change("pairs", pairs);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
serialize = () => {
|
|
109
|
+
return EditorJsonify.serialize.call(this);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type IframeEditorProps = $FlowFixMe;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* This is the main editor for this widget, to specify all the options.
|
|
117
|
+
*/
|
|
118
|
+
class IframeEditor extends React.Component<IframeEditorProps> {
|
|
119
|
+
static propTypes = {
|
|
120
|
+
...Changeable.propTypes,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
static widgetName: "iframe" = "iframe";
|
|
124
|
+
|
|
125
|
+
static defaultProps: IframeEditorProps = {
|
|
126
|
+
url: "",
|
|
127
|
+
settings: [{name: "", value: ""}],
|
|
128
|
+
width: "400",
|
|
129
|
+
height: "400",
|
|
130
|
+
allowFullScreen: false,
|
|
131
|
+
allowTopNavigation: false,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
|
|
135
|
+
return Changeable.change.apply(this, args);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
render(): React.Node {
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
<div style={{fontWeight: "bold", textAlign: "center"}}>
|
|
142
|
+
This widget is deprecated! <br />
|
|
143
|
+
Try using the Video or CS Program widgets instead.
|
|
144
|
+
</div>
|
|
145
|
+
<label>
|
|
146
|
+
Url or Program ID:
|
|
147
|
+
<BlurInput
|
|
148
|
+
value={this.props.url}
|
|
149
|
+
onChange={this.change("url")}
|
|
150
|
+
/>
|
|
151
|
+
</label>
|
|
152
|
+
<br />
|
|
153
|
+
<label>
|
|
154
|
+
Settings:
|
|
155
|
+
<PairsEditor
|
|
156
|
+
name="settings"
|
|
157
|
+
pairs={this.props.settings}
|
|
158
|
+
onChange={this.handleSettingsChange}
|
|
159
|
+
/>
|
|
160
|
+
</label>
|
|
161
|
+
<br />
|
|
162
|
+
<label>
|
|
163
|
+
Width:
|
|
164
|
+
<BlurInput
|
|
165
|
+
value={this.props.width}
|
|
166
|
+
onChange={this.change("width")}
|
|
167
|
+
/>
|
|
168
|
+
</label>
|
|
169
|
+
<label>
|
|
170
|
+
Height:
|
|
171
|
+
<BlurInput
|
|
172
|
+
value={this.props.height}
|
|
173
|
+
onChange={this.change("height")}
|
|
174
|
+
/>
|
|
175
|
+
</label>
|
|
176
|
+
<PropCheckBox
|
|
177
|
+
label="Allow full screen"
|
|
178
|
+
allowFullScreen={this.props.allowFullScreen}
|
|
179
|
+
onChange={this.props.onChange}
|
|
180
|
+
/>
|
|
181
|
+
<br />
|
|
182
|
+
<PropCheckBox
|
|
183
|
+
label="Allow iframe content to redirect the page"
|
|
184
|
+
allowTopNavigation={this.props.allowTopNavigation}
|
|
185
|
+
onChange={this.props.onChange}
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
handleSettingsChange: ($FlowFixMe) => void = (settings) => {
|
|
192
|
+
this.change({settings: settings.pairs});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
serialize: () => $FlowFixMe = () => {
|
|
196
|
+
return EditorJsonify.serialize.call(this);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default IframeEditor;
|