@khanacademy/perseus-editor 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/es/index.js +10 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.js +10 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/strings.js +6 -1
  8. package/package.json +16 -4
  9. package/src/README.md +7 -0
  10. package/src/__stories__/editor.stories.js +33 -0
  11. package/src/__stories__/section-control-button.stories.jsx +27 -0
  12. package/src/__testdata__/input-number_testdata.js +28 -0
  13. package/src/__tests__/editor_test.jsx +123 -0
  14. package/src/__tests__/i18n_test.jsx +585 -0
  15. package/src/__tests__/traversal_test.jsx +378 -0
  16. package/src/all-editors.js +85 -0
  17. package/src/all-editors_test.js +17 -0
  18. package/src/all-widgets.js +10 -0
  19. package/src/article-editor.jsx +437 -0
  20. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  21. package/src/components/blur-input.jsx +62 -0
  22. package/src/components/checkbox.jsx +194 -0
  23. package/src/components/device-framer.jsx +91 -0
  24. package/src/components/drag-target.jsx +88 -0
  25. package/src/components/dropdown-option.jsx +318 -0
  26. package/src/components/form-wrapped-text-field.jsx +248 -0
  27. package/src/components/graph-settings.jsx +631 -0
  28. package/src/components/hover-behavior.jsx +178 -0
  29. package/src/components/json-editor.jsx +118 -0
  30. package/src/components/link.jsx +173 -0
  31. package/src/components/simple-button.jsx +86 -0
  32. package/src/components/sortable.jsx +186 -0
  33. package/src/components/util.js +39 -0
  34. package/src/components/viewport-resizer.jsx +73 -0
  35. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  36. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  37. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  38. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  39. package/src/diffs/__tests__/split-diff_test.js +34 -0
  40. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  41. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  42. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  43. package/src/diffs/article-diff.jsx +70 -0
  44. package/src/diffs/item-diff.jsx +77 -0
  45. package/src/diffs/renderer-diff.jsx +143 -0
  46. package/src/diffs/split-diff.jsx +30 -0
  47. package/src/diffs/string-array-diff.jsx +74 -0
  48. package/src/diffs/structured-item-diff.jsx +269 -0
  49. package/src/diffs/tags-diff.jsx +50 -0
  50. package/src/diffs/text-diff.jsx +193 -0
  51. package/src/diffs/widget-diff-performer.jsx +74 -0
  52. package/src/diffs/widget-diff.jsx +291 -0
  53. package/src/editor-page.jsx +351 -0
  54. package/src/editor.jsx +1364 -0
  55. package/src/hint-editor.jsx +447 -0
  56. package/src/i18n.jsx +139 -0
  57. package/src/iframe-content-renderer.jsx +201 -0
  58. package/src/index.js +27 -8
  59. package/src/item-editor.jsx +159 -0
  60. package/src/item-extras-editor.jsx +150 -0
  61. package/src/katex-error-view.jsx +87 -0
  62. package/src/multirenderer-editor.jsx +1073 -0
  63. package/src/section-control-button.jsx +53 -0
  64. package/src/stateful-article-editor.jsx +176 -0
  65. package/src/stateful-editor-page.jsx +90 -0
  66. package/src/styles/perseus-editor.less +1339 -0
  67. package/src/testing-widgets.js +12 -0
  68. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  69. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  70. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  71. package/src/widgets/categorizer-editor.jsx +103 -0
  72. package/src/widgets/cs-program-editor.jsx +258 -0
  73. package/src/widgets/definition-editor.jsx +76 -0
  74. package/src/widgets/dropdown-editor.jsx +201 -0
  75. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  76. package/src/widgets/example-graphie-widget.jsx +175 -0
  77. package/src/widgets/example-widget-editor.jsx +60 -0
  78. package/src/widgets/example-widget.jsx +153 -0
  79. package/src/widgets/explanation-editor.jsx +89 -0
  80. package/src/widgets/expression-editor.jsx +559 -0
  81. package/src/widgets/graded-group-editor.jsx +162 -0
  82. package/src/widgets/graded-group-set-editor.jsx +101 -0
  83. package/src/widgets/grapher-editor.jsx +154 -0
  84. package/src/widgets/group-editor.jsx +83 -0
  85. package/src/widgets/iframe-editor.jsx +200 -0
  86. package/src/widgets/image-editor.jsx +326 -0
  87. package/src/widgets/input-number-editor.jsx +269 -0
  88. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  89. package/src/widgets/interaction-editor.jsx +1388 -0
  90. package/src/widgets/interactive-graph-editor.jsx +320 -0
  91. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  92. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  93. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  94. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  95. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  96. package/src/widgets/label-image/answer-choices.jsx +231 -0
  97. package/src/widgets/label-image/behavior.jsx +92 -0
  98. package/src/widgets/label-image/marker.jsx +292 -0
  99. package/src/widgets/label-image/question-markers.jsx +181 -0
  100. package/src/widgets/label-image/select-image.jsx +86 -0
  101. package/src/widgets/label-image-editor.jsx +256 -0
  102. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  103. package/src/widgets/matcher-editor.jsx +147 -0
  104. package/src/widgets/matrix-editor.jsx +155 -0
  105. package/src/widgets/measurer-editor.jsx +245 -0
  106. package/src/widgets/molecule-editor.jsx +63 -0
  107. package/src/widgets/number-line-editor.jsx +498 -0
  108. package/src/widgets/numeric-input-editor.jsx +554 -0
  109. package/src/widgets/orderer-editor.jsx +166 -0
  110. package/src/widgets/passage-editor.jsx +115 -0
  111. package/src/widgets/passage-ref-editor.jsx +77 -0
  112. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  113. package/src/widgets/plotter-editor.jsx +537 -0
  114. package/src/widgets/radio/editor.jsx +399 -0
  115. package/src/widgets/reaction-diagram-editor.jsx +126 -0
  116. package/src/widgets/sequence-editor.jsx +205 -0
  117. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  118. package/src/widgets/simple-markdown-tester.jsx +91 -0
  119. package/src/widgets/simulator-editor.jsx +144 -0
  120. package/src/widgets/sorter-editor.jsx +100 -0
  121. package/src/widgets/table-editor.jsx +164 -0
  122. package/src/widgets/transformer-editor.jsx +1006 -0
  123. package/src/widgets/unit-editor.jsx +293 -0
  124. package/src/widgets/video-editor.jsx +74 -0
  125. package/src/__tests__/index.test.js +0 -15
@@ -0,0 +1,399 @@
1
+ /* eslint-disable jsx-a11y/anchor-is-valid, react/forbid-prop-types, react/sort-comp */
2
+ // @flow
3
+ import {
4
+ components,
5
+ icons,
6
+ ApiOptions,
7
+ BaseRadio,
8
+ Changeable,
9
+ } from "@khanacademy/perseus";
10
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
11
+ import PropTypes from "prop-types";
12
+ import * as React from "react";
13
+ import _ from "underscore";
14
+
15
+ import Editor from "../../editor.jsx";
16
+
17
+ const {InlineIcon, PropCheckBox} = components;
18
+ const {iconPlus, iconTrash} = icons;
19
+
20
+ class ChoiceEditor extends React.Component<$FlowFixMe> {
21
+ static propTypes = {
22
+ apiOptions: ApiOptions.propTypes,
23
+
24
+ choice: PropTypes.object,
25
+ showDelete: PropTypes.bool,
26
+ onClueChange: PropTypes.func,
27
+ onContentChange: PropTypes.func,
28
+ onDelete: PropTypes.func,
29
+ };
30
+
31
+ render(): React.Node {
32
+ const checkedClass = this.props.choice.correct
33
+ ? "correct"
34
+ : "incorrect";
35
+ let placeholder = "Type a choice here...";
36
+
37
+ if (this.props.choice.isNoneOfTheAbove) {
38
+ placeholder = this.props.choice.correct
39
+ ? "Type the answer to reveal to the user..."
40
+ : "None of the above";
41
+ }
42
+
43
+ const editor = (
44
+ <Editor
45
+ // eslint-disable-next-line react/no-string-refs
46
+ ref="content-editor"
47
+ apiOptions={this.props.apiOptions}
48
+ content={this.props.choice.content || ""}
49
+ widgetEnabled={false}
50
+ placeholder={placeholder}
51
+ disabled={
52
+ this.props.choice.isNoneOfTheAbove &&
53
+ !this.props.choice.correct
54
+ }
55
+ onChange={this.props.onContentChange}
56
+ />
57
+ );
58
+
59
+ const clueEditor = (
60
+ <Editor
61
+ // eslint-disable-next-line react/no-string-refs
62
+ ref="clue-editor"
63
+ apiOptions={this.props.apiOptions}
64
+ content={this.props.choice.clue || ""}
65
+ widgetEnabled={false}
66
+ placeholder={i18n._(
67
+ "Why is this choice %(correctOrIncorrect)s?",
68
+ {correctOrIncorrect: checkedClass},
69
+ )}
70
+ onChange={this.props.onClueChange}
71
+ />
72
+ );
73
+
74
+ const deleteLink = (
75
+ <a
76
+ className="simple-button orange delete-choice"
77
+ href="#"
78
+ onClick={this.props.onDelete}
79
+ title="Remove this choice"
80
+ >
81
+ <InlineIcon {...iconTrash} />
82
+ </a>
83
+ );
84
+
85
+ return (
86
+ <div className="choice-clue-editors">
87
+ <div className={`choice-editor ${checkedClass}`}>{editor}</div>
88
+ <div className="clue-editor">{clueEditor}</div>
89
+ {this.props.showDelete && deleteLink}
90
+ </div>
91
+ );
92
+ }
93
+ }
94
+
95
+ class RadioEditor extends React.Component<$FlowFixMe> {
96
+ static propTypes = {
97
+ ...Changeable.propTypes,
98
+ apiOptions: ApiOptions.propTypes,
99
+ choices: PropTypes.arrayOf(
100
+ PropTypes.shape({
101
+ content: PropTypes.string,
102
+ clue: PropTypes.string,
103
+ correct: PropTypes.bool,
104
+ }),
105
+ ),
106
+ displayCount: PropTypes.number,
107
+ randomize: PropTypes.bool,
108
+ hasNoneOfTheAbove: PropTypes.bool,
109
+ multipleSelect: PropTypes.bool,
110
+ countChoices: PropTypes.bool,
111
+
112
+ // TODO(kevinb): DEPRECATED: This is be used to force deselectEnabled
113
+ // behavior on mobile but not on desktop. When enabled, the user can
114
+ // deselect a radio input by tapping on it again.
115
+ deselectEnabled: PropTypes.bool,
116
+
117
+ static: PropTypes.bool,
118
+ };
119
+
120
+ static widgetName: string = "radio";
121
+
122
+ static defaultProps: $FlowFixMe = {
123
+ choices: [{}, {}],
124
+ displayCount: null,
125
+ randomize: false,
126
+ hasNoneOfTheAbove: false,
127
+ multipleSelect: false,
128
+ countChoices: false,
129
+ deselectEnabled: false,
130
+ };
131
+
132
+ render(): React.Node {
133
+ const numCorrect = _.reduce(
134
+ this.props.choices,
135
+ function (memo, choice) {
136
+ return choice.correct ? memo + 1 : memo;
137
+ },
138
+ 0,
139
+ );
140
+ return (
141
+ <div>
142
+ <div className="perseus-widget-row">
143
+ <a
144
+ href={
145
+ // This is an editor component, not user-facing.
146
+ // eslint-disable-next-line static-service/require-safe-link-to
147
+ "https://docs.google.com/document/d/1frZf7yrWVWb1n4tVjqlzqVUiv1pn4cZXbxgP62-JDBY/edit#heading=h.8ng1isya19nu"
148
+ }
149
+ target="_blank"
150
+ >
151
+ Multiple choice style guide
152
+ </a>
153
+ <br />
154
+ <div className="perseus-widget-left-col">
155
+ <PropCheckBox
156
+ label="Multiple selections"
157
+ labelAlignment="right"
158
+ multipleSelect={this.props.multipleSelect}
159
+ onChange={this.onMultipleSelectChange}
160
+ />
161
+ </div>
162
+ <div className="perseus-widget-right-col">
163
+ <PropCheckBox
164
+ label="Randomize order"
165
+ labelAlignment="right"
166
+ randomize={this.props.randomize}
167
+ onChange={this.props.onChange}
168
+ />
169
+ </div>
170
+ {this.props.multipleSelect && (
171
+ <div className="perseus-widget-left-col">
172
+ <PropCheckBox
173
+ label="Specify number correct"
174
+ labelAlignment="right"
175
+ countChoices={this.props.countChoices}
176
+ onChange={this.onCountChoicesChange}
177
+ />
178
+ </div>
179
+ )}
180
+ </div>
181
+
182
+ <BaseRadio
183
+ // eslint-disable-next-line react/no-string-refs
184
+ ref="baseRadio"
185
+ multipleSelect={this.props.multipleSelect}
186
+ countChoices={this.props.countChoices}
187
+ numCorrect={numCorrect}
188
+ editMode={true}
189
+ labelWrap={false}
190
+ apiOptions={this.props.apiOptions}
191
+ choices={this.props.choices.map(function (choice, i) {
192
+ return {
193
+ content: (
194
+ <ChoiceEditor
195
+ ref={`choice-editor${i}`}
196
+ apiOptions={this.props.apiOptions}
197
+ choice={choice}
198
+ onContentChange={(newProps) => {
199
+ if ("content" in newProps) {
200
+ this.onContentChange(
201
+ i,
202
+ newProps.content,
203
+ );
204
+ }
205
+ }}
206
+ onClueChange={(newProps) => {
207
+ if ("content" in newProps) {
208
+ this.onClueChange(
209
+ i,
210
+ newProps.content,
211
+ );
212
+ }
213
+ }}
214
+ // eslint-disable-next-line react/jsx-no-bind
215
+ onDelete={this.onDelete.bind(this, i)}
216
+ showDelete={this.props.choices.length >= 2}
217
+ />
218
+ ),
219
+ isNoneOfTheAbove: choice.isNoneOfTheAbove,
220
+ checked: choice.correct,
221
+ };
222
+ }, this)}
223
+ onChange={this.onChange}
224
+ />
225
+
226
+ <div className="add-choice-container">
227
+ <a
228
+ className="simple-button orange"
229
+ href="#"
230
+ // eslint-disable-next-line react/jsx-no-bind
231
+ onClick={this.addChoice.bind(this, false)}
232
+ >
233
+ <InlineIcon {...iconPlus} /> Add a choice{" "}
234
+ </a>
235
+
236
+ {!this.props.hasNoneOfTheAbove && (
237
+ <a
238
+ className="simple-button"
239
+ href="#"
240
+ // eslint-disable-next-line react/jsx-no-bind
241
+ onClick={this.addChoice.bind(this, true)}
242
+ >
243
+ <InlineIcon {...iconPlus} /> None of the above{" "}
244
+ </a>
245
+ )}
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ change: (...args: $ReadOnlyArray<mixed>) => $FlowFixMe = (...args) => {
252
+ // $FlowFixMe[incompatible-call]
253
+ return Changeable.change.apply(this, args);
254
+ };
255
+
256
+ onMultipleSelectChange: ($FlowFixMe) => $FlowFixMe = (allowMultiple) => {
257
+ allowMultiple = allowMultiple.multipleSelect;
258
+
259
+ const numCorrect = _.reduce(
260
+ this.props.choices,
261
+ function (memo, choice) {
262
+ return choice.correct ? memo + 1 : memo;
263
+ },
264
+ 0,
265
+ );
266
+
267
+ if (!allowMultiple && numCorrect > 1) {
268
+ const choices = _.map(this.props.choices, function (choice) {
269
+ return _.defaults(
270
+ {
271
+ correct: false,
272
+ },
273
+ choice,
274
+ );
275
+ });
276
+ this.props.onChange({
277
+ multipleSelect: allowMultiple,
278
+ choices: choices,
279
+ });
280
+ } else {
281
+ this.props.onChange({
282
+ multipleSelect: allowMultiple,
283
+ });
284
+ }
285
+ };
286
+
287
+ onCountChoicesChange: ($FlowFixMe) => void = (count) => {
288
+ count = count.countChoices;
289
+ this.props.onChange({countChoices: count});
290
+ };
291
+
292
+ onChange: ($FlowFixMe) => void = ({checked}) => {
293
+ const choices = _.map(this.props.choices, (choice, i) => {
294
+ return _.extend({}, choice, {
295
+ correct: checked[i],
296
+ content:
297
+ choice.isNoneOfTheAbove && !checked[i]
298
+ ? ""
299
+ : choice.content,
300
+ });
301
+ });
302
+
303
+ this.props.onChange({choices: choices});
304
+ };
305
+
306
+ onContentChange: ($FlowFixMe, $FlowFixMe) => void = (
307
+ choiceIndex,
308
+ newContent,
309
+ ) => {
310
+ const choices = this.props.choices.slice();
311
+ choices[choiceIndex] = _.extend({}, choices[choiceIndex], {
312
+ content: newContent,
313
+ });
314
+ this.props.onChange({choices: choices});
315
+ };
316
+
317
+ onClueChange: ($FlowFixMe, $FlowFixMe) => void = (choiceIndex, newClue) => {
318
+ const choices = this.props.choices.slice();
319
+ choices[choiceIndex] = _.extend({}, choices[choiceIndex], {
320
+ clue: newClue,
321
+ });
322
+ if (newClue === "") {
323
+ delete choices[choiceIndex].clue;
324
+ }
325
+ this.props.onChange({choices: choices});
326
+ };
327
+
328
+ onDelete: (number, $FlowFixMe) => void = (choiceIndex, e) => {
329
+ e.preventDefault();
330
+
331
+ const choices = this.props.choices.slice();
332
+ const deleted = choices[choiceIndex];
333
+
334
+ choices.splice(choiceIndex, 1);
335
+
336
+ this.props.onChange({
337
+ choices: choices,
338
+ hasNoneOfTheAbove:
339
+ this.props.hasNoneOfTheAbove && !deleted.isNoneOfTheAbove,
340
+ });
341
+ };
342
+
343
+ addChoice: (boolean, $FlowFixMe) => void = (noneOfTheAbove, e) => {
344
+ e.preventDefault();
345
+
346
+ const choices = this.props.choices.slice();
347
+ const newChoice = {isNoneOfTheAbove: noneOfTheAbove};
348
+ const addIndex =
349
+ choices.length - (this.props.hasNoneOfTheAbove ? 1 : 0);
350
+
351
+ choices.splice(addIndex, 0, newChoice);
352
+
353
+ this.props.onChange(
354
+ {
355
+ choices: choices,
356
+ hasNoneOfTheAbove:
357
+ noneOfTheAbove || this.props.hasNoneOfTheAbove,
358
+ },
359
+ () => {
360
+ // eslint-disable-next-line react/no-string-refs
361
+ this.refs[`choice-editor${addIndex}`].refs[
362
+ "content-editor"
363
+ ].focus();
364
+ },
365
+ );
366
+ };
367
+
368
+ setDisplayCount: (number) => void = (num) => {
369
+ this.props.onChange({displayCount: num});
370
+ };
371
+
372
+ focus: () => boolean = () => {
373
+ // eslint-disable-next-line react/no-string-refs
374
+ this.refs["choice-editor0"].refs["content-editor"].focus();
375
+ return true;
376
+ };
377
+
378
+ getSaveWarnings: () => $ReadOnlyArray<string> = () => {
379
+ if (!_.some(_.pluck(this.props.choices, "correct"))) {
380
+ return ["No choice is marked as correct."];
381
+ }
382
+ return [];
383
+ };
384
+
385
+ serialize: () => $FlowFixMe = () => {
386
+ return _.pick(
387
+ this.props,
388
+ "choices",
389
+ "randomize",
390
+ "multipleSelect",
391
+ "countChoices",
392
+ "displayCount",
393
+ "hasNoneOfTheAbove",
394
+ "deselectEnabled",
395
+ );
396
+ };
397
+ }
398
+
399
+ export default RadioEditor;
@@ -0,0 +1,126 @@
1
+ // @flow
2
+ import {components, Changeable, EditorJsonify} from "@khanacademy/perseus";
3
+ import PropTypes from "prop-types";
4
+ import * as React from "react";
5
+
6
+ const {NumberInput, TextInput} = components;
7
+
8
+ type Props = $FlowFixMe;
9
+
10
+ class ReactionDiagramWidgetEditor extends React.Component<Props> {
11
+ static propTypes = {
12
+ ...Changeable.propTypes,
13
+ rotationAngle: PropTypes.arrayOf(PropTypes.number),
14
+ separators: PropTypes.arrayOf(PropTypes.object),
15
+ smiles: PropTypes.arrayOf(PropTypes.string),
16
+ };
17
+
18
+ static widgetName: "reaction-diagram" = "reaction-diagram";
19
+
20
+ static defaultProps: Props = {
21
+ smiles: ["", ""],
22
+ rotationAngle: [0, 0],
23
+ separators: [{type: "right", topText: "", bottomText: ""}],
24
+ };
25
+
26
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
27
+ return Changeable.change.apply(this, args);
28
+ };
29
+
30
+ updateMolecule: (number) => ($FlowFixMe) => void = (idx) => {
31
+ return function (newValue) {
32
+ const newSmiles = [...this.props.smiles];
33
+ newSmiles[idx] = newValue;
34
+ this.change({smiles: newSmiles});
35
+ }.bind(this);
36
+ };
37
+
38
+ updateRotation: (number) => ($FlowFixMe) => void = (idx) => {
39
+ return function (newValue) {
40
+ const newRot = [...this.props.rotationAngle];
41
+ newRot[idx] = newValue;
42
+ this.change({rotationAngle: newRot});
43
+ }.bind(this);
44
+ };
45
+
46
+ updateSeparators: (number, string) => ($FlowFixMe) => void = (
47
+ idx,
48
+ propName,
49
+ ) => {
50
+ return (newValue) => {
51
+ const newSep = this.props.separators.map((sep) => {
52
+ return {...sep};
53
+ });
54
+ newSep[idx][propName] = newValue;
55
+ this.change({separators: newSep});
56
+ };
57
+ };
58
+
59
+ serialize: () => $FlowFixMe = () => {
60
+ return EditorJsonify.serialize.call(this);
61
+ };
62
+
63
+ render(): React.Node {
64
+ // TODO(colin): use styling instead of &nbsp hacks.
65
+ return (
66
+ <div>
67
+ <div>
68
+ <label>
69
+ LHS SMILES:&nbsp;
70
+ <TextInput
71
+ onChange={this.updateMolecule(0)}
72
+ value={this.props.smiles[0]}
73
+ />
74
+ </label>
75
+ </div>
76
+ <div>
77
+ <label>
78
+ LHS Rotation (deg):&nbsp;
79
+ <NumberInput
80
+ onChange={this.updateRotation(0)}
81
+ value={this.props.rotationAngle[0]}
82
+ />
83
+ </label>
84
+ </div>
85
+ <div>
86
+ <label>
87
+ RHS SMILES:&nbsp;
88
+ <TextInput
89
+ onChange={this.updateMolecule(1)}
90
+ value={this.props.smiles[1]}
91
+ />
92
+ </label>
93
+ </div>
94
+ <div>
95
+ <label>
96
+ RHS Rotation (deg):&nbsp;
97
+ <NumberInput
98
+ onChange={this.updateRotation(1)}
99
+ value={this.props.rotationAngle[1]}
100
+ />
101
+ </label>
102
+ </div>
103
+ <div>
104
+ <label>
105
+ Top of arrow text:&nbsp;
106
+ <TextInput
107
+ onChange={this.updateSeparators(0, "topText")}
108
+ value={this.props.separators[0].topText}
109
+ />
110
+ </label>
111
+ </div>
112
+ <div>
113
+ <label>
114
+ Bottom of arrow text:&nbsp;
115
+ <TextInput
116
+ onChange={this.updateSeparators(0, "bottomText")}
117
+ value={this.props.separators[0].bottomText}
118
+ />
119
+ </label>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+ }
125
+
126
+ export default ReactionDiagramWidgetEditor;