@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.
Files changed (127) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/es/index.css +1020 -0
  4. package/dist/es/index.js +9 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +1020 -0
  7. package/dist/index.js +9 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +6 -1
  10. package/package.json +16 -4
  11. package/src/README.md +7 -0
  12. package/src/__stories__/editor.stories.js +33 -0
  13. package/src/__stories__/section-control-button.stories.jsx +27 -0
  14. package/src/__testdata__/input-number_testdata.js +28 -0
  15. package/src/__tests__/editor_test.jsx +123 -0
  16. package/src/__tests__/i18n_test.jsx +585 -0
  17. package/src/__tests__/traversal_test.jsx +378 -0
  18. package/src/all-editors.js +85 -0
  19. package/src/all-editors_test.js +17 -0
  20. package/src/all-widgets.js +10 -0
  21. package/src/article-editor.jsx +437 -0
  22. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  23. package/src/components/blur-input.jsx +62 -0
  24. package/src/components/checkbox.jsx +194 -0
  25. package/src/components/device-framer.jsx +91 -0
  26. package/src/components/drag-target.jsx +88 -0
  27. package/src/components/dropdown-option.jsx +318 -0
  28. package/src/components/form-wrapped-text-field.jsx +248 -0
  29. package/src/components/graph-settings.jsx +631 -0
  30. package/src/components/hover-behavior.jsx +178 -0
  31. package/src/components/json-editor.jsx +121 -0
  32. package/src/components/link.jsx +173 -0
  33. package/src/components/simple-button.jsx +86 -0
  34. package/src/components/sortable.jsx +186 -0
  35. package/src/components/util.js +39 -0
  36. package/src/components/viewport-resizer.jsx +73 -0
  37. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  38. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  39. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  40. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  41. package/src/diffs/__tests__/split-diff_test.js +34 -0
  42. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  43. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  44. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  45. package/src/diffs/article-diff.jsx +70 -0
  46. package/src/diffs/item-diff.jsx +77 -0
  47. package/src/diffs/renderer-diff.jsx +143 -0
  48. package/src/diffs/split-diff.jsx +30 -0
  49. package/src/diffs/string-array-diff.jsx +75 -0
  50. package/src/diffs/structured-item-diff.jsx +269 -0
  51. package/src/diffs/tags-diff.jsx +50 -0
  52. package/src/diffs/text-diff.jsx +194 -0
  53. package/src/diffs/widget-diff-performer.jsx +74 -0
  54. package/src/diffs/widget-diff.jsx +291 -0
  55. package/src/editor-page.jsx +351 -0
  56. package/src/editor.jsx +1364 -0
  57. package/src/hint-editor.jsx +447 -0
  58. package/src/i18n.jsx +139 -0
  59. package/src/iframe-content-renderer.jsx +201 -0
  60. package/src/index.js +27 -8
  61. package/src/item-editor.jsx +159 -0
  62. package/src/item-extras-editor.jsx +150 -0
  63. package/src/katex-error-view.jsx +87 -0
  64. package/src/multirenderer-editor.jsx +1074 -0
  65. package/src/section-control-button.jsx +53 -0
  66. package/src/stateful-article-editor.jsx +176 -0
  67. package/src/stateful-editor-page.jsx +90 -0
  68. package/src/styles/perseus-editor.less +1339 -0
  69. package/src/testing-widgets.js +12 -0
  70. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  71. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  72. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  73. package/src/widgets/categorizer-editor.jsx +103 -0
  74. package/src/widgets/cs-program-editor.jsx +258 -0
  75. package/src/widgets/definition-editor.jsx +75 -0
  76. package/src/widgets/dropdown-editor.jsx +201 -0
  77. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  78. package/src/widgets/example-graphie-widget.jsx +175 -0
  79. package/src/widgets/example-widget-editor.jsx +60 -0
  80. package/src/widgets/example-widget.jsx +153 -0
  81. package/src/widgets/explanation-editor.jsx +89 -0
  82. package/src/widgets/expression-editor.jsx +560 -0
  83. package/src/widgets/graded-group-editor.jsx +162 -0
  84. package/src/widgets/graded-group-set-editor.jsx +101 -0
  85. package/src/widgets/grapher-editor.jsx +154 -0
  86. package/src/widgets/group-editor.jsx +83 -0
  87. package/src/widgets/iframe-editor.jsx +200 -0
  88. package/src/widgets/image-editor.jsx +326 -0
  89. package/src/widgets/input-number-editor.jsx +269 -0
  90. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  91. package/src/widgets/interaction-editor.jsx +1389 -0
  92. package/src/widgets/interactive-graph-editor.jsx +322 -0
  93. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  94. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  95. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  96. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  97. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  98. package/src/widgets/label-image/answer-choices.jsx +231 -0
  99. package/src/widgets/label-image/behavior.jsx +92 -0
  100. package/src/widgets/label-image/marker.jsx +292 -0
  101. package/src/widgets/label-image/question-markers.jsx +181 -0
  102. package/src/widgets/label-image/select-image.jsx +86 -0
  103. package/src/widgets/label-image-editor.jsx +257 -0
  104. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  105. package/src/widgets/matcher-editor.jsx +147 -0
  106. package/src/widgets/matrix-editor.jsx +155 -0
  107. package/src/widgets/measurer-editor.jsx +245 -0
  108. package/src/widgets/molecule-editor.jsx +63 -0
  109. package/src/widgets/number-line-editor.jsx +498 -0
  110. package/src/widgets/numeric-input-editor.jsx +554 -0
  111. package/src/widgets/orderer-editor.jsx +166 -0
  112. package/src/widgets/passage-editor.jsx +115 -0
  113. package/src/widgets/passage-ref-editor.jsx +77 -0
  114. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  115. package/src/widgets/plotter-editor.jsx +537 -0
  116. package/src/widgets/radio/editor.jsx +398 -0
  117. package/src/widgets/reaction-diagram-editor.jsx +127 -0
  118. package/src/widgets/sequence-editor.jsx +205 -0
  119. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  120. package/src/widgets/simple-markdown-tester.jsx +91 -0
  121. package/src/widgets/simulator-editor.jsx +144 -0
  122. package/src/widgets/sorter-editor.jsx +100 -0
  123. package/src/widgets/table-editor.jsx +164 -0
  124. package/src/widgets/transformer-editor.jsx +1006 -0
  125. package/src/widgets/unit-editor.jsx +293 -0
  126. package/src/widgets/video-editor.jsx +74 -0
  127. 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
- import {foo} from "@khanacademy/perseus";
3
- import * as i18n from "@khanacademy/wonder-blocks-i18n";
4
-
5
- export const bar = (): void => {
6
- const msg = i18n._("foo() = %(foo)s", {foo: foo()});
7
- // eslint-disable-next-line no-console
8
- console.log(msg);
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
+ &nbsp; 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;