@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
|
+
/**
|
|
3
|
+
* Displays the given content in an iframe, isolating it from the parent page
|
|
4
|
+
*
|
|
5
|
+
* To simulate the environment of content rendered by itself, content previews
|
|
6
|
+
* are rendered inside iframes, where components such as the math keypad work
|
|
7
|
+
* because the body of the document is not the body of the editor. To make this
|
|
8
|
+
* work, this component renders an iframe and can communicate objects to it
|
|
9
|
+
* through postMessage. The recipient then needs to listen for these messages
|
|
10
|
+
* and pull out the appropriate object stored in the parent's iframeDataStore
|
|
11
|
+
* to get the data to render. When the iframe is loaded, it's javascript calls
|
|
12
|
+
* its requestIframeData function in the parent, which triggers the parent to
|
|
13
|
+
* send the current data.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {Log} from "@khanacademy/perseus";
|
|
17
|
+
import createReactClass from "create-react-class";
|
|
18
|
+
import PropTypes from "prop-types";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
|
|
21
|
+
let nextIframeID = 0;
|
|
22
|
+
const requestIframeData = {};
|
|
23
|
+
const updateIframeHeight = {};
|
|
24
|
+
window.iframeDataStore = {};
|
|
25
|
+
|
|
26
|
+
// This is called once after Perseus is loaded and the iframe
|
|
27
|
+
// is ready to render content, then twice a second afterwards
|
|
28
|
+
// to capture the result of animations.
|
|
29
|
+
window.addEventListener("message", (event) => {
|
|
30
|
+
if (typeof event.data === "string") {
|
|
31
|
+
// In Perseus, we expect the callback to exist, as it is added by
|
|
32
|
+
// `IframeContentRenderer.componentDidMount()`. Unfortunately, this
|
|
33
|
+
// event listener also gets added in Manticore (since we include Perseus
|
|
34
|
+
// from there), and Crowdin fires its own "message" events. So we'll
|
|
35
|
+
// just have to ignore the event when we can't find the callback.
|
|
36
|
+
const callback = requestIframeData[event.data];
|
|
37
|
+
if (callback) {
|
|
38
|
+
callback();
|
|
39
|
+
}
|
|
40
|
+
} else if (event.data.id) {
|
|
41
|
+
if (event.data.height !== undefined) {
|
|
42
|
+
updateIframeHeight[event.data.id](event.data.height);
|
|
43
|
+
} else if (event.data.lintWarnings) {
|
|
44
|
+
// This is a lint report being sent back from the linter.
|
|
45
|
+
// TODO:
|
|
46
|
+
// We'll want to display the number of warnings in the HUD.
|
|
47
|
+
// But for now, we just log it to the console
|
|
48
|
+
Log.log("LINTER REPORT", {
|
|
49
|
+
lintWarnings: JSON.stringify(event.data.lintWarnings),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const IframeContentRenderer: $FlowFixMe = createReactClass({
|
|
56
|
+
displayName: "IframeContentRenderer",
|
|
57
|
+
|
|
58
|
+
propTypes: {
|
|
59
|
+
// The HTML content to render to the iframe
|
|
60
|
+
content: PropTypes.string.isRequired,
|
|
61
|
+
|
|
62
|
+
// The data-* suffix for passing information to the iframe's JS
|
|
63
|
+
datasetKey: PropTypes.any,
|
|
64
|
+
|
|
65
|
+
// The value of the data-* attribute
|
|
66
|
+
datasetValue: PropTypes.any,
|
|
67
|
+
|
|
68
|
+
// Whether to make the iframe's height match its content's height,
|
|
69
|
+
// used to prevent scrolling inside the iframe.
|
|
70
|
+
seamless: PropTypes.bool,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
componentDidMount: function () {
|
|
74
|
+
// TODO(scottgrant): This is a hack to remove the deprecated call to
|
|
75
|
+
// this.isMounted() but is still considered an anti-pattern.
|
|
76
|
+
this._isMounted = true;
|
|
77
|
+
|
|
78
|
+
this.iframeID = nextIframeID;
|
|
79
|
+
nextIframeID++;
|
|
80
|
+
|
|
81
|
+
this._prepareFrame();
|
|
82
|
+
requestIframeData[this.iframeID] = () => {
|
|
83
|
+
this.sendNewData(this._lastData);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
updateIframeHeight[this.iframeID] = (height) => {
|
|
87
|
+
this._lastHeight = height;
|
|
88
|
+
if (this._isMounted && this.props.seamless) {
|
|
89
|
+
// eslint-disable-next-line react/no-string-refs
|
|
90
|
+
this.refs.container.style.height = height + "px";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
shouldComponentUpdate: function (nextProps) {
|
|
96
|
+
return (
|
|
97
|
+
nextProps.content !== this.props.content ||
|
|
98
|
+
nextProps.datasetValue !== this.props.datasetValue ||
|
|
99
|
+
nextProps.seamless !== this.props.seamless
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
componentDidUpdate: function (prevProps) {
|
|
104
|
+
if (!this.props.seamless) {
|
|
105
|
+
// eslint-disable-next-line react/no-string-refs
|
|
106
|
+
this.refs.container.style.height = "100%";
|
|
107
|
+
} else {
|
|
108
|
+
// eslint-disable-next-line react/no-string-refs
|
|
109
|
+
this.refs.container.style.height = this._lastHeight + "px";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
prevProps.content !== this.props.content ||
|
|
114
|
+
prevProps.datasetValue !== this.props.datasetValue
|
|
115
|
+
) {
|
|
116
|
+
// Not just a change in seamless
|
|
117
|
+
this._prepareFrame();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
componentWillUnmount: function () {
|
|
122
|
+
requestIframeData[this.iframeID] = null;
|
|
123
|
+
updateIframeHeight[this.iframeID] = null;
|
|
124
|
+
|
|
125
|
+
this._isMounted = false;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
_prepareFrame: function () {
|
|
129
|
+
// Don't initialize the iframe until the page has loaded
|
|
130
|
+
if (this._frame) {
|
|
131
|
+
// eslint-disable-next-line react/no-string-refs
|
|
132
|
+
this.refs.container.removeChild(this._frame);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this._frame = document.createElement("iframe");
|
|
136
|
+
this._frame.style.width = "100%";
|
|
137
|
+
this._frame.style.height = "100%";
|
|
138
|
+
|
|
139
|
+
if (this.props.datasetKey) {
|
|
140
|
+
// If the user has specified a data-* attribute to place on the
|
|
141
|
+
// iframe, we set it here. Right now, this is used to
|
|
142
|
+
// communicate if the iframe should be enabling touch emulation.
|
|
143
|
+
this._frame.dataset[this.props.datasetKey] =
|
|
144
|
+
this.props.datasetValue;
|
|
145
|
+
}
|
|
146
|
+
this._frame.dataset.id = this.iframeID;
|
|
147
|
+
|
|
148
|
+
if (this.props.seamless) {
|
|
149
|
+
// The seamless prop is the same as the "nochrome" prop that
|
|
150
|
+
// gets passed to DeviceFramer. If it is set, then we're going
|
|
151
|
+
// to be displaying editor previews and want to leave some room
|
|
152
|
+
// for lint indicators in the right margin. We use the dataset
|
|
153
|
+
// as above to pass this information on to the perseus-frame
|
|
154
|
+
// component inside the iframe
|
|
155
|
+
this._frame.dataset.lintGutter = "true";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// To make sure the value of location.href inside the iframe is the
|
|
159
|
+
// same as the location of the parent, we wait for the iframe to
|
|
160
|
+
// load before writing contents. Without the wait, the location
|
|
161
|
+
// inside the iframe becomes "about:blank", which causes problems
|
|
162
|
+
// with loading $LAB.
|
|
163
|
+
this._frame.onload = () => {
|
|
164
|
+
this._frame.onload = null;
|
|
165
|
+
|
|
166
|
+
// To prevent an issue with the contents of the iframe not being
|
|
167
|
+
// loaded properly, where the javascript inside the iframe is
|
|
168
|
+
// not executed, we push the content window write to the end
|
|
169
|
+
// of the event queue.
|
|
170
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
171
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
this._frame.contentWindow.document.open();
|
|
174
|
+
this._frame.contentWindow.document.write(this.props.content);
|
|
175
|
+
this._frame.contentWindow.document.close();
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// eslint-disable-next-line react/no-string-refs
|
|
180
|
+
this.refs.container.appendChild(this._frame);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
sendNewData: function (data) {
|
|
184
|
+
if (this._isMounted && data && this._frame.contentWindow) {
|
|
185
|
+
this._lastData = data;
|
|
186
|
+
|
|
187
|
+
// We can't use JSON.stringify/parse for this because the apiOptions
|
|
188
|
+
// includes the functions GroupMetadataEditor, groupAnnotator,
|
|
189
|
+
// onFocusChange, and onInputError.
|
|
190
|
+
window.iframeDataStore[this.iframeID] = data;
|
|
191
|
+
this._frame.contentWindow.postMessage(this.iframeID, "*");
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
render: function () {
|
|
196
|
+
// eslint-disable-next-line react/no-string-refs
|
|
197
|
+
return <div ref="container" style={{width: "100%", height: "100%"}} />;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export default IframeContentRenderer;
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
2
|
+
export * as Perseus from "@khanacademy/perseus";
|
|
3
|
+
|
|
4
|
+
export {default as ArticleEditor} from "./article-editor.jsx";
|
|
5
|
+
export {default as DeviceFramer} from "./components/device-framer.jsx";
|
|
6
|
+
export {default as ViewportResizer} from "./components/viewport-resizer.jsx";
|
|
7
|
+
export {default as ArticleDiff} from "./diffs/article-diff.jsx";
|
|
8
|
+
export {default as ItemDiff} from "./diffs/item-diff.jsx";
|
|
9
|
+
export {default as StructuredItemDiff} from "./diffs/structured-item-diff.jsx";
|
|
10
|
+
export {default as EditorPage} from "./editor-page.jsx";
|
|
11
|
+
export {default as Editor} from "./editor.jsx";
|
|
12
|
+
export {default as i18n} from "./i18n.jsx";
|
|
13
|
+
export {default as IframeContentRenderer} from "./iframe-content-renderer.jsx";
|
|
14
|
+
export {default as MultiRendererEditor} from "./multirenderer-editor.jsx";
|
|
15
|
+
export {default as StatefulArticleEditor} from "./stateful-article-editor.jsx";
|
|
16
|
+
export {default as StatefulEditorPage} from "./stateful-editor-page.jsx";
|
|
17
|
+
|
|
18
|
+
import "./styles/perseus-editor.less";
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line import/order
|
|
21
|
+
import {Widgets} from "@khanacademy/perseus";
|
|
22
|
+
import AllEditors from "./all-editors.js";
|
|
23
|
+
import AllWidgets from "./all-widgets.js";
|
|
24
|
+
|
|
25
|
+
Widgets.registerEditors(AllEditors);
|
|
26
|
+
Widgets.registerWidgets(AllWidgets);
|
|
27
|
+
|
|
28
|
+
export {AllEditors, AllWidgets};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {itemDataVersion} from "@khanacademy/perseus";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import _ from "underscore";
|
|
5
|
+
|
|
6
|
+
import DeviceFramer from "./components/device-framer.jsx";
|
|
7
|
+
import Editor from "./editor.jsx";
|
|
8
|
+
import IframeContentRenderer from "./iframe-content-renderer.jsx";
|
|
9
|
+
import ItemExtrasEditor from "./item-extras-editor.jsx";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
APIOptions,
|
|
13
|
+
ImageUploader,
|
|
14
|
+
ChangeHandler,
|
|
15
|
+
} from "@khanacademy/perseus";
|
|
16
|
+
|
|
17
|
+
const ITEM_DATA_VERSION = itemDataVersion;
|
|
18
|
+
|
|
19
|
+
type Props = {|
|
|
20
|
+
apiOptions?: APIOptions,
|
|
21
|
+
deviceType?: string,
|
|
22
|
+
frameSource: string,
|
|
23
|
+
gradeMessage?: string,
|
|
24
|
+
imageUploader?: ImageUploader,
|
|
25
|
+
wasAnswered?: boolean,
|
|
26
|
+
question?: $FlowFixMe,
|
|
27
|
+
answerArea?: $FlowFixMe,
|
|
28
|
+
onChange: ChangeHandler,
|
|
29
|
+
|
|
30
|
+
// The content ID of the AssessmentItem being edited. It may not be set
|
|
31
|
+
// for non-content library exercise questions.
|
|
32
|
+
itemId?: string,
|
|
33
|
+
|};
|
|
34
|
+
|
|
35
|
+
class ItemEditor extends React.Component<Props> {
|
|
36
|
+
static defaultProps: {|
|
|
37
|
+
answerArea: {...},
|
|
38
|
+
onChange: () => void,
|
|
39
|
+
question: {...},
|
|
40
|
+
|} = {
|
|
41
|
+
onChange: () => {},
|
|
42
|
+
question: {},
|
|
43
|
+
answerArea: {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Notify the parent that the question or answer area has been updated.
|
|
47
|
+
updateProps: ChangeHandler = (newProps, cb, silent) => {
|
|
48
|
+
const props = _(this.props).pick("question", "answerArea");
|
|
49
|
+
|
|
50
|
+
this.props.onChange(_(props).extend(newProps), cb, silent);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
triggerPreviewUpdate: (newData: any) => void = (newData: $FlowFixMe) => {
|
|
54
|
+
// eslint-disable-next-line react/no-string-refs
|
|
55
|
+
this.refs.frame.sendNewData(newData);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
handleEditorChange: ChangeHandler = (newProps, cb, silent) => {
|
|
59
|
+
const question = _.extend({}, this.props.question, newProps);
|
|
60
|
+
this.updateProps({question}, cb, silent);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
handleItemExtrasChange: ChangeHandler = (newProps, cb, silent) => {
|
|
64
|
+
const answerArea = _.extend({}, this.props.answerArea, newProps);
|
|
65
|
+
this.updateProps({answerArea}, cb, silent);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
getSaveWarnings: () => any = () => {
|
|
69
|
+
// eslint-disable-next-line react/no-string-refs
|
|
70
|
+
return this.refs.questionEditor.getSaveWarnings();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
serialize: (options: any) => {|
|
|
74
|
+
answerArea: any,
|
|
75
|
+
itemDataVersion: {|major: number, minor: number|},
|
|
76
|
+
question: any,
|
|
77
|
+
|} = (options: $FlowFixMe) => {
|
|
78
|
+
return {
|
|
79
|
+
// eslint-disable-next-line react/no-string-refs
|
|
80
|
+
question: this.refs.questionEditor.serialize(options),
|
|
81
|
+
// eslint-disable-next-line react/no-string-refs
|
|
82
|
+
answerArea: this.refs.itemExtrasEditor.serialize(options),
|
|
83
|
+
itemDataVersion: ITEM_DATA_VERSION,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
render(): React.Element<"div"> {
|
|
88
|
+
const isMobile =
|
|
89
|
+
this.props.deviceType === "phone" ||
|
|
90
|
+
this.props.deviceType === "tablet";
|
|
91
|
+
return (
|
|
92
|
+
<div className="perseus-editor-table">
|
|
93
|
+
<div className="perseus-editor-row perseus-question-container">
|
|
94
|
+
<div className="perseus-editor-left-cell">
|
|
95
|
+
<div className="pod-title">Question</div>
|
|
96
|
+
<Editor
|
|
97
|
+
// eslint-disable-next-line react/no-string-refs
|
|
98
|
+
ref="questionEditor"
|
|
99
|
+
// Using the AssessmentItem content ID as the key
|
|
100
|
+
// ensures that when the user navigates to another
|
|
101
|
+
// item in the Sidebar, the question editor is
|
|
102
|
+
// re-rendered by React.
|
|
103
|
+
key={this.props.itemId}
|
|
104
|
+
placeholder="Type your question here..."
|
|
105
|
+
className="perseus-question-editor"
|
|
106
|
+
imageUploader={this.props.imageUploader}
|
|
107
|
+
onChange={this.handleEditorChange}
|
|
108
|
+
apiOptions={this.props.apiOptions}
|
|
109
|
+
showWordCount={true}
|
|
110
|
+
{...this.props.question}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="perseus-editor-right-cell">
|
|
115
|
+
<div id="problemarea">
|
|
116
|
+
<DeviceFramer
|
|
117
|
+
deviceType={this.props.deviceType}
|
|
118
|
+
nochrome={true}
|
|
119
|
+
>
|
|
120
|
+
<IframeContentRenderer
|
|
121
|
+
// eslint-disable-next-line react/no-string-refs
|
|
122
|
+
ref="frame"
|
|
123
|
+
key={this.props.deviceType}
|
|
124
|
+
content={this.props.frameSource}
|
|
125
|
+
datasetKey="mobile"
|
|
126
|
+
datasetValue={isMobile}
|
|
127
|
+
seamless={true}
|
|
128
|
+
/>
|
|
129
|
+
</DeviceFramer>
|
|
130
|
+
<div
|
|
131
|
+
id="hintsarea"
|
|
132
|
+
className="hintsarea"
|
|
133
|
+
style={{display: "none"}}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="perseus-editor-row perseus-answer-container">
|
|
140
|
+
<div className="perseus-editor-left-cell">
|
|
141
|
+
<div className="pod-title">Question extras</div>
|
|
142
|
+
<ItemExtrasEditor
|
|
143
|
+
// eslint-disable-next-line react/no-string-refs
|
|
144
|
+
ref="itemExtrasEditor"
|
|
145
|
+
onChange={this.handleItemExtrasChange}
|
|
146
|
+
{...this.props.answerArea}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="perseus-editor-right-cell">
|
|
151
|
+
<div id="answer_area" />
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default ItemEditor;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {components} from "@khanacademy/perseus";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
const {InfoTip} = components;
|
|
7
|
+
|
|
8
|
+
type Props = $FlowFixMe;
|
|
9
|
+
|
|
10
|
+
class ItemExtrasEditor extends React.Component<Props> {
|
|
11
|
+
static propTypes = {
|
|
12
|
+
calculator: PropTypes.bool,
|
|
13
|
+
chi2Table: PropTypes.bool,
|
|
14
|
+
onChange: PropTypes.func.isRequired,
|
|
15
|
+
periodicTable: PropTypes.bool,
|
|
16
|
+
tTable: PropTypes.bool,
|
|
17
|
+
zTable: PropTypes.bool,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static defaultProps: Props = {
|
|
21
|
+
calculator: false,
|
|
22
|
+
chi2Table: false,
|
|
23
|
+
periodicTable: false,
|
|
24
|
+
tTable: false,
|
|
25
|
+
zTable: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
serialize: () => $FlowFixMe = () => {
|
|
29
|
+
return {
|
|
30
|
+
calculator: this.props.calculator,
|
|
31
|
+
chi2Table: this.props.chi2Table,
|
|
32
|
+
periodicTable: this.props.periodicTable,
|
|
33
|
+
tTable: this.props.tTable,
|
|
34
|
+
zTable: this.props.zTable,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
render(): React.Node {
|
|
39
|
+
return (
|
|
40
|
+
<div className="perseus-answer-editor">
|
|
41
|
+
<div className="perseus-answer-options">
|
|
42
|
+
<div>
|
|
43
|
+
<label>
|
|
44
|
+
Show calculator:{" "}
|
|
45
|
+
<input
|
|
46
|
+
type="checkbox"
|
|
47
|
+
checked={this.props.calculator}
|
|
48
|
+
onChange={(e) => {
|
|
49
|
+
this.props.onChange({
|
|
50
|
+
calculator: e.target.checked,
|
|
51
|
+
});
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</label>
|
|
55
|
+
<InfoTip>
|
|
56
|
+
Use the calculator when completing difficult
|
|
57
|
+
calculations is NOT the intent of the question.
|
|
58
|
+
DON’T use the calculator when testing the student’s
|
|
59
|
+
ability to complete different types of computations.
|
|
60
|
+
</InfoTip>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<label>
|
|
65
|
+
Show periodic table:{" "}
|
|
66
|
+
<input
|
|
67
|
+
type="checkbox"
|
|
68
|
+
checked={this.props.periodicTable}
|
|
69
|
+
onChange={(e) => {
|
|
70
|
+
this.props.onChange({
|
|
71
|
+
periodicTable: e.target.checked,
|
|
72
|
+
});
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
</label>
|
|
76
|
+
<InfoTip>
|
|
77
|
+
This provides the student with the ability to view a
|
|
78
|
+
periodic table of the elements, e.g., for answering
|
|
79
|
+
chemistry questions.
|
|
80
|
+
</InfoTip>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div>
|
|
84
|
+
<label>
|
|
85
|
+
Show z table (statistics):{" "}
|
|
86
|
+
<input
|
|
87
|
+
type="checkbox"
|
|
88
|
+
checked={this.props.zTable}
|
|
89
|
+
onChange={(e) => {
|
|
90
|
+
this.props.onChange({
|
|
91
|
+
zTable: e.target.checked,
|
|
92
|
+
});
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
</label>
|
|
96
|
+
<InfoTip>
|
|
97
|
+
This provides the student with the ability to view a
|
|
98
|
+
table of critical values for the z distribution,
|
|
99
|
+
e.g. for answering statistics questions.
|
|
100
|
+
</InfoTip>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div>
|
|
104
|
+
<label>
|
|
105
|
+
Show t table (statistics):{" "}
|
|
106
|
+
<input
|
|
107
|
+
type="checkbox"
|
|
108
|
+
checked={this.props.tTable}
|
|
109
|
+
onChange={(e) => {
|
|
110
|
+
this.props.onChange({
|
|
111
|
+
tTable: e.target.checked,
|
|
112
|
+
});
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
</label>
|
|
116
|
+
<InfoTip>
|
|
117
|
+
This provides the student with the ability to view a
|
|
118
|
+
table of critical values for the Student's t
|
|
119
|
+
distribution, e.g. for answering statistics
|
|
120
|
+
questions.
|
|
121
|
+
</InfoTip>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div>
|
|
125
|
+
<label>
|
|
126
|
+
Show chi-squared table (statistics):{" "}
|
|
127
|
+
<input
|
|
128
|
+
type="checkbox"
|
|
129
|
+
checked={this.props.chi2Table}
|
|
130
|
+
onChange={(e) => {
|
|
131
|
+
this.props.onChange({
|
|
132
|
+
chi2Table: e.target.checked,
|
|
133
|
+
});
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</label>
|
|
137
|
+
<InfoTip>
|
|
138
|
+
This provides the student with the ability to view a
|
|
139
|
+
table of critical values for the chi-squared
|
|
140
|
+
distribution, e.g. for answering statistics
|
|
141
|
+
questions.
|
|
142
|
+
</InfoTip>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default ItemExtrasEditor;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Displays a collapsable list of KaTeX rendering errors.
|
|
4
|
+
*/
|
|
5
|
+
import {css, StyleSheet} from "aphrodite";
|
|
6
|
+
import PropTypes from "prop-types";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
|
|
9
|
+
type Props = $FlowFixMe;
|
|
10
|
+
type State = $FlowFixMe;
|
|
11
|
+
|
|
12
|
+
class KatexErrorView extends React.Component<Props, State> {
|
|
13
|
+
static propTypes = {
|
|
14
|
+
errorList: PropTypes.arrayOf(
|
|
15
|
+
PropTypes.shape({
|
|
16
|
+
math: PropTypes.string.isRequired,
|
|
17
|
+
message: PropTypes.string.isRequired,
|
|
18
|
+
}),
|
|
19
|
+
).isRequired,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
state: State = {
|
|
23
|
+
showErrors: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
handleToggleKatexErrors: (SyntheticMouseEvent<>) => void = (e) => {
|
|
27
|
+
this.setState({showErrors: !this.state.showErrors});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
render(): React.Node {
|
|
31
|
+
const {errorList} = this.props;
|
|
32
|
+
const {showErrors} = this.state;
|
|
33
|
+
|
|
34
|
+
// TODO(riley) replace with SVG icons
|
|
35
|
+
const disclosureClass = showErrors
|
|
36
|
+
? "icon-chevron-down"
|
|
37
|
+
: "icon-chevron-right";
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={css(styles.errorContainer)}>
|
|
41
|
+
<div
|
|
42
|
+
className={css(styles.title)}
|
|
43
|
+
onClick={this.handleToggleKatexErrors}
|
|
44
|
+
>
|
|
45
|
+
<i className={disclosureClass} style={{fontSize: 14}} />
|
|
46
|
+
KaTeX Errors ({errorList.length})
|
|
47
|
+
</div>
|
|
48
|
+
{showErrors && (
|
|
49
|
+
<div className={css(styles.errorExplanation)}>
|
|
50
|
+
These errors will cause your LaTeX to load really slowly
|
|
51
|
+
for the student. Please fix them if you can. If you
|
|
52
|
+
can’t because KaTeX doesn’t support the feature you
|
|
53
|
+
need, please message Cam.
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{showErrors &&
|
|
57
|
+
errorList.map((e, index) => (
|
|
58
|
+
<div className={css(styles.error)} key={index}>
|
|
59
|
+
<div style={{color: "red"}}>{e.math}</div>
|
|
60
|
+
<div>{e.message}</div>
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const styles = StyleSheet.create({
|
|
69
|
+
title: {
|
|
70
|
+
backgroundColor: "#eee",
|
|
71
|
+
fontSize: "1.25em",
|
|
72
|
+
padding: "4px 10px",
|
|
73
|
+
},
|
|
74
|
+
errorContainer: {
|
|
75
|
+
border: "1px solid #ddd",
|
|
76
|
+
borderTop: "none",
|
|
77
|
+
},
|
|
78
|
+
errorExplanation: {
|
|
79
|
+
padding: "4px 10px",
|
|
80
|
+
backgroundColor: "pink",
|
|
81
|
+
},
|
|
82
|
+
error: {
|
|
83
|
+
padding: "4px 10px",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export default KatexErrorView;
|