@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,631 @@
1
+ /* eslint-disable react/forbid-prop-types, react/no-unsafe */
2
+ // @flow
3
+ import {
4
+ components,
5
+ interactiveSizes,
6
+ Changeable,
7
+ Dependencies,
8
+ KhanMath,
9
+ Util,
10
+ } from "@khanacademy/perseus";
11
+ import createReactClass from "create-react-class";
12
+ import PropTypes from "prop-types";
13
+ import * as React from "react";
14
+ import ReactDOM from "react-dom";
15
+ import _ from "underscore";
16
+
17
+ const {ButtonGroup, InfoTip, PropCheckBox, RangeInput} = components;
18
+ const {getDependencies} = Dependencies;
19
+
20
+ const defaultBackgroundImage = {
21
+ url: null,
22
+ width: 0,
23
+ height: 0,
24
+ };
25
+
26
+ function numSteps(range, step) {
27
+ return Math.floor((range[1] - range[0]) / step);
28
+ }
29
+
30
+ // $FlowFixMe[signature-verification-failure]
31
+ const GraphSettings = createReactClass({
32
+ displayName: "GraphSettings",
33
+
34
+ propTypes: {
35
+ ...Changeable.propTypes,
36
+ editableSettings: PropTypes.arrayOf(
37
+ PropTypes.oneOf(["canvas", "graph", "snap", "image", "measure"]),
38
+ ),
39
+ box: PropTypes.arrayOf(PropTypes.number),
40
+ labels: PropTypes.arrayOf(PropTypes.string),
41
+ range: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
42
+ step: PropTypes.arrayOf(PropTypes.number),
43
+ gridStep: PropTypes.arrayOf(PropTypes.number),
44
+ snapStep: PropTypes.arrayOf(PropTypes.number),
45
+ valid: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
46
+ backgroundImage: PropTypes.object,
47
+ markings: PropTypes.oneOf(["graph", "grid", "none"]),
48
+ showProtractor: PropTypes.bool,
49
+ showRuler: PropTypes.bool,
50
+ showTooltips: PropTypes.bool,
51
+ rulerLabel: PropTypes.string,
52
+ rulerTicks: PropTypes.number,
53
+ },
54
+
55
+ getDefaultProps: function () {
56
+ return {
57
+ editableSettings: ["graph", "snap", "image", "measure"],
58
+ box: [
59
+ interactiveSizes.defaultBoxSizeSmall,
60
+ interactiveSizes.defaultBoxSizeSmall,
61
+ ],
62
+ labels: ["x", "y"],
63
+ range: [
64
+ [-10, 10],
65
+ [-10, 10],
66
+ ],
67
+ step: [1, 1],
68
+ gridStep: [1, 1],
69
+ snapStep: [1, 1],
70
+ valid: true,
71
+ backgroundImage: defaultBackgroundImage,
72
+ markings: "graph",
73
+ showProtractor: false,
74
+ showRuler: false,
75
+ showTooltips: false,
76
+ rulerLabel: "",
77
+ rulerTicks: 10,
78
+ };
79
+ },
80
+
81
+ getInitialState: function () {
82
+ return this.stateFromProps(this.props);
83
+ },
84
+
85
+ componentDidMount: function () {
86
+ // TODO(scottgrant): This is a hack to remove the deprecated call to
87
+ // this.isMounted() but is still considered an anti-pattern.
88
+ this._isMounted = true;
89
+
90
+ this.changeGraph = _.debounce(this.changeGraph, 300);
91
+ },
92
+
93
+ UNSAFE_componentWillReceiveProps: function (nextProps) {
94
+ // Make sure that state updates when switching
95
+ // between different items in a multi-item editor.
96
+ if (
97
+ !_.isEqual(this.props.labels, nextProps.labels) ||
98
+ !_.isEqual(this.props.gridStep, nextProps.gridStep) ||
99
+ !_.isEqual(this.props.snapStep, nextProps.snapStep) ||
100
+ !_.isEqual(this.props.step, nextProps.step) ||
101
+ !_.isEqual(this.props.range, nextProps.range) ||
102
+ !_.isEqual(this.props.backgroundImage, nextProps.backgroundImage)
103
+ ) {
104
+ this.setState(this.stateFromProps(nextProps));
105
+ }
106
+ },
107
+
108
+ componentWillUnmount: function () {
109
+ this._isMounted = false;
110
+ },
111
+
112
+ stateFromProps: function (props) {
113
+ return {
114
+ labelsTextbox: props.labels,
115
+ gridStepTextbox: props.gridStep,
116
+ snapStepTextbox: props.snapStep,
117
+ stepTextbox: props.step,
118
+ rangeTextbox: props.range,
119
+ backgroundImage: _.clone(props.backgroundImage),
120
+ };
121
+ },
122
+
123
+ change(...args) {
124
+ // $FlowFixMe[object-this-reference]
125
+ return Changeable.change.apply(this, args);
126
+ },
127
+
128
+ // TODO(aria): Make either a wrapper for standard events to work
129
+ // with this.change, or make these use some TextInput/NumberInput box
130
+ changeRulerLabel: function (e) {
131
+ this.change({rulerLabel: e.target.value});
132
+ },
133
+
134
+ changeRulerTicks: function (e) {
135
+ this.change({rulerTicks: +e.target.value});
136
+ },
137
+
138
+ changeBackgroundUrl: function (e) {
139
+ // Only continue on blur or "enter"
140
+ if (e.type === "keypress" && e.key !== "Enter") {
141
+ return;
142
+ }
143
+
144
+ const setUrl = (url, width, height) => {
145
+ const image = _.clone(this.props.backgroundImage);
146
+ image.url = url;
147
+ image.width = width;
148
+ image.height = height;
149
+ this.setState(
150
+ {
151
+ backgroundImage: image,
152
+ },
153
+ this.changeGraph,
154
+ );
155
+ };
156
+
157
+ // $FlowFixMe[incompatible-use]
158
+ // $FlowFixMe[prop-missing]
159
+ const url = ReactDOM.findDOMNode(this.refs["bg-url"]).value; // eslint-disable-line react/no-string-refs
160
+ if (url) {
161
+ Util.getImageSize(url, (width, height) => {
162
+ if (this._isMounted) {
163
+ setUrl(url, width, height);
164
+ }
165
+ });
166
+ } else {
167
+ setUrl(null, 0, 0);
168
+ }
169
+ },
170
+
171
+ renderLabelChoices: function (choices) {
172
+ return _.map(choices, function (nameAndValue) {
173
+ return <option value={nameAndValue[1]}>{nameAndValue[0]}</option>;
174
+ });
175
+ },
176
+
177
+ validRange: function (range) {
178
+ const numbers = _.every(range, function (num) {
179
+ return _.isFinite(num);
180
+ });
181
+ if (!numbers) {
182
+ return "Range must be a valid number";
183
+ }
184
+ if (range[0] >= range[1]) {
185
+ return "Range must have a higher number on the right";
186
+ }
187
+ return true;
188
+ },
189
+
190
+ validateStepValue: function (settings) {
191
+ const {step, range, name, minTicks, maxTicks} = settings;
192
+
193
+ if (!_.isFinite(step)) {
194
+ return name + " must be a valid number";
195
+ }
196
+ const nSteps = numSteps(range, step);
197
+ if (nSteps < minTicks) {
198
+ return (
199
+ name +
200
+ " is too large, there must be at least " +
201
+ minTicks +
202
+ " ticks."
203
+ );
204
+ }
205
+ if (nSteps > maxTicks) {
206
+ return (
207
+ name +
208
+ " is too small, there can be at most " +
209
+ maxTicks +
210
+ " ticks."
211
+ );
212
+ }
213
+ return true;
214
+ },
215
+
216
+ validSnapStep: function (step, range) {
217
+ return this.validateStepValue({
218
+ step: step,
219
+ range: range,
220
+ name: "Snap step",
221
+ minTicks: 5,
222
+ maxTicks: 60,
223
+ });
224
+ },
225
+
226
+ validGridStep: function (step, range) {
227
+ return this.validateStepValue({
228
+ step: step,
229
+ range: range,
230
+ name: "Grid step",
231
+ minTicks: 3,
232
+ maxTicks: 60,
233
+ });
234
+ },
235
+
236
+ validStep: function (step, range) {
237
+ return this.validateStepValue({
238
+ step: step,
239
+ range: range,
240
+ name: "Step",
241
+ minTicks: 3,
242
+ maxTicks: 20,
243
+ });
244
+ },
245
+
246
+ validBackgroundImageSize: function (image) {
247
+ // Ignore empty images
248
+ if (!image.url) {
249
+ return true;
250
+ }
251
+
252
+ const validSize = image.width <= 450 && image.height <= 450;
253
+
254
+ if (!validSize) {
255
+ return "Image must be smaller than 450px x 450px.";
256
+ }
257
+ return true;
258
+ },
259
+
260
+ validateGraphSettings: function (range, step, gridStep, snapStep, image) {
261
+ const self = this;
262
+ let msg;
263
+ const goodRange = _.every(range, function (range) {
264
+ msg = self.validRange(range);
265
+ return msg === true;
266
+ });
267
+ if (!goodRange) {
268
+ return msg;
269
+ }
270
+ const goodStep = _.every(step, function (step, i) {
271
+ msg = self.validStep(step, range[i]);
272
+ return msg === true;
273
+ });
274
+ if (!goodStep) {
275
+ return msg;
276
+ }
277
+ const goodGridStep = _.every(gridStep, function (gridStep, i) {
278
+ msg = self.validGridStep(gridStep, range[i]);
279
+ return msg === true;
280
+ });
281
+ if (!goodGridStep) {
282
+ return msg;
283
+ }
284
+ const goodSnapStep = _.every(snapStep, function (snapStep, i) {
285
+ msg = self.validSnapStep(snapStep, range[i]);
286
+ return msg === true;
287
+ });
288
+ if (!goodSnapStep) {
289
+ return msg;
290
+ }
291
+ const goodImageSize = this.validBackgroundImageSize(image);
292
+ if (goodImageSize !== true) {
293
+ msg = goodImageSize;
294
+ return msg;
295
+ }
296
+ return true;
297
+ },
298
+
299
+ changeLabel: function (i, e) {
300
+ const val = e.target.value;
301
+ const labels = this.state.labelsTextbox.slice();
302
+ labels[i] = val;
303
+ this.setState({labelsTextbox: labels}, this.changeGraph);
304
+ },
305
+
306
+ changeRange: function (i, values) {
307
+ const ranges = this.state.rangeTextbox.slice();
308
+ ranges[i] = values;
309
+ const step = this.state.stepTextbox.slice();
310
+ const gridStep = this.state.gridStepTextbox.slice();
311
+ const snapStep = this.state.snapStepTextbox.slice();
312
+ const scale = Util.scaleFromExtent(ranges[i], this.props.box[i]);
313
+ if (this.validRange(ranges[i]) === true) {
314
+ step[i] = Util.tickStepFromExtent(ranges[i], this.props.box[i]);
315
+ gridStep[i] = Util.gridStepFromTickStep(step[i], scale);
316
+ snapStep[i] = gridStep[i] / 2;
317
+ }
318
+ this.setState(
319
+ {
320
+ stepTextbox: step,
321
+ gridStepTextbox: gridStep,
322
+ snapStepTextbox: snapStep,
323
+ rangeTextbox: ranges,
324
+ },
325
+ this.changeGraph,
326
+ );
327
+ },
328
+
329
+ changeStep: function (step) {
330
+ this.setState({stepTextbox: step}, this.changeGraph);
331
+ },
332
+
333
+ changeSnapStep: function (snapStep) {
334
+ this.setState({snapStepTextbox: snapStep}, this.changeGraph);
335
+ },
336
+
337
+ changeGridStep: function (gridStep) {
338
+ this.setState(
339
+ {
340
+ gridStepTextbox: gridStep,
341
+ snapStepTextbox: _.map(gridStep, function (step) {
342
+ return step / 2;
343
+ }),
344
+ },
345
+ this.changeGraph,
346
+ );
347
+ },
348
+
349
+ changeGraph: function () {
350
+ const labels = this.state.labelsTextbox;
351
+ const range = _.map(this.state.rangeTextbox, function (range) {
352
+ return _.map(range, Number);
353
+ });
354
+ const step = _.map(this.state.stepTextbox, Number);
355
+ const gridStep = this.state.gridStepTextbox;
356
+ const snapStep = this.state.snapStepTextbox;
357
+ const image = this.state.backgroundImage;
358
+
359
+ // validationResult is either:
360
+ // true -> the settings are valid
361
+ // a string -> the settings are invalid, and the explanation
362
+ // is contained in the string
363
+ // TODO(aria): Refactor this to not be confusing
364
+ const validationResult = this.validateGraphSettings(
365
+ range,
366
+ step,
367
+ gridStep,
368
+ snapStep,
369
+ image,
370
+ );
371
+
372
+ if (validationResult === true) {
373
+ // either true or a string
374
+ this.change({
375
+ valid: true,
376
+ labels: labels,
377
+ range: range,
378
+ step: step,
379
+ gridStep: gridStep,
380
+ snapStep: snapStep,
381
+ backgroundImage: image,
382
+ });
383
+ } else {
384
+ this.change({
385
+ valid: validationResult, // a string message, not false
386
+ });
387
+ }
388
+ },
389
+
390
+ render: function () {
391
+ const scale = [
392
+ KhanMath.roundTo(
393
+ 2,
394
+ Util.scaleFromExtent(this.props.range[0], this.props.box[0]),
395
+ ),
396
+ KhanMath.roundTo(
397
+ 2,
398
+ Util.scaleFromExtent(this.props.range[1], this.props.box[1]),
399
+ ),
400
+ ];
401
+
402
+ const {TeX} = getDependencies();
403
+
404
+ return (
405
+ <div>
406
+ {_.contains(this.props.editableSettings, "canvas") && (
407
+ <div className="graph-settings">
408
+ <div className="perseus-widget-row">
409
+ Canvas size (x,y pixels)
410
+ <RangeInput
411
+ value={this.props.box}
412
+ onChange={(box) => {
413
+ this.change({box: box});
414
+ }}
415
+ />
416
+ </div>
417
+ <div className="perseus-widget-row">
418
+ Scale (px per div):{" "}
419
+ <TeX>{"(" + scale[0] + ", " + scale[1] + ")"}</TeX>
420
+ </div>
421
+ </div>
422
+ )}
423
+
424
+ {_.contains(this.props.editableSettings, "graph") && (
425
+ <div className="graph-settings">
426
+ <div className="perseus-widget-row">
427
+ <div className="perseus-widget-left-col">
428
+ {" "}
429
+ x Label
430
+ <input
431
+ type="text"
432
+ className="graph-settings-axis-label"
433
+ // eslint-disable-next-line react/no-string-refs
434
+ ref="labels-0"
435
+ onChange={(e) => this.changeLabel(0, e)}
436
+ value={this.state.labelsTextbox[0] || ""}
437
+ />
438
+ </div>
439
+ <div className="perseus-widget-right-col">
440
+ y Label
441
+ <input
442
+ type="text"
443
+ className="graph-settings-axis-label"
444
+ // eslint-disable-next-line react/no-string-refs
445
+ ref="labels-1"
446
+ onChange={(e) => this.changeLabel(1, e)}
447
+ value={this.state.labelsTextbox[1] || ""}
448
+ />
449
+ </div>
450
+ </div>
451
+
452
+ <div className="perseus-widget-row">
453
+ <div className="perseus-widget-left-col">
454
+ x Range
455
+ <RangeInput
456
+ value={this.state.rangeTextbox[0]}
457
+ onChange={(vals) =>
458
+ this.changeRange(0, vals)
459
+ }
460
+ />
461
+ </div>
462
+ <div className="perseus-widget-right-col">
463
+ y Range
464
+ <RangeInput
465
+ value={this.state.rangeTextbox[1]}
466
+ onChange={(vals) =>
467
+ this.changeRange(1, vals)
468
+ }
469
+ />
470
+ </div>
471
+ </div>
472
+ <div className="perseus-widget-row">
473
+ <div className="perseus-widget-left-col">
474
+ Tick Step
475
+ <RangeInput
476
+ value={this.state.stepTextbox}
477
+ onChange={this.changeStep}
478
+ />
479
+ </div>
480
+ <div className="perseus-widget-right-col">
481
+ Grid Step
482
+ <RangeInput
483
+ value={this.state.gridStepTextbox}
484
+ onChange={this.changeGridStep}
485
+ />
486
+ </div>
487
+ </div>
488
+ {_.contains(this.props.editableSettings, "snap") && (
489
+ <div className="perseus-widget-row">
490
+ <div className="perseus-widget-left-col">
491
+ Snap Step
492
+ <RangeInput
493
+ value={this.state.snapStepTextbox}
494
+ onChange={this.changeSnapStep}
495
+ />
496
+ </div>
497
+ </div>
498
+ )}
499
+ <div className="perseus-widget-row">
500
+ <label>Markings: </label>
501
+ <ButtonGroup
502
+ value={this.props.markings}
503
+ allowEmpty={false}
504
+ buttons={[
505
+ {value: "graph", content: "Graph"},
506
+ {value: "grid", content: "Grid"},
507
+ {value: "none", content: "None"},
508
+ ]}
509
+ onChange={this.change("markings")}
510
+ />
511
+ </div>
512
+ <div className="perseus-widget-left-col">
513
+ <PropCheckBox
514
+ label="Show tooltips"
515
+ showTooltips={this.props.showTooltips}
516
+ onChange={this.change}
517
+ />
518
+ </div>
519
+ </div>
520
+ )}
521
+
522
+ {_.contains(this.props.editableSettings, "image") && (
523
+ <div className="image-settings">
524
+ <div>Background image:</div>
525
+ <div>
526
+ Url:{" "}
527
+ <input
528
+ type="text"
529
+ className="graph-settings-background-url"
530
+ // eslint-disable-next-line react/no-string-refs
531
+ ref="bg-url"
532
+ value={this.state.backgroundImage.url || ""}
533
+ onChange={(e) => {
534
+ const image = _.clone(
535
+ this.props.backgroundImage,
536
+ );
537
+ image.url = e.target.value;
538
+ this.setState({backgroundImage: image});
539
+ }}
540
+ onKeyPress={this.changeBackgroundUrl}
541
+ onBlur={this.changeBackgroundUrl}
542
+ />
543
+ <InfoTip>
544
+ <p>
545
+ Create an image in graphie, or use the "Add
546
+ image" function to create a background.
547
+ </p>
548
+ </InfoTip>
549
+ </div>
550
+ </div>
551
+ )}
552
+
553
+ {_.contains(this.props.editableSettings, "measure") && (
554
+ <div className="misc-settings">
555
+ <div className="perseus-widget-row">
556
+ <div className="perseus-widget-left-col">
557
+ <PropCheckBox
558
+ label="Show ruler"
559
+ showRuler={this.props.showRuler}
560
+ onChange={this.change}
561
+ />
562
+ </div>
563
+ <div className="perseus-widget-right-col">
564
+ <PropCheckBox
565
+ label="Show protractor"
566
+ showProtractor={this.props.showProtractor}
567
+ onChange={this.change}
568
+ />
569
+ </div>
570
+ </div>
571
+ {this.props.showRuler && (
572
+ <div>
573
+ <div>
574
+ <label>
575
+ {" "}
576
+ Ruler label:{" "}
577
+ <select
578
+ onChange={this.changeRulerLabel}
579
+ value={this.props.rulerLabel}
580
+ >
581
+ <option value="">None</option>
582
+ <optgroup label="Metric">
583
+ {this.renderLabelChoices([
584
+ ["milimeters", "mm"],
585
+ ["centimeters", "cm"],
586
+ ["meters", "m"],
587
+ ["kilometers", "km"],
588
+ ])}
589
+ </optgroup>
590
+ <optgroup label="Imperial">
591
+ {this.renderLabelChoices([
592
+ ["inches", "in"],
593
+ ["feet", "ft"],
594
+ ["yards", "yd"],
595
+ ["miles", "mi"],
596
+ ])}
597
+ </optgroup>
598
+ </select>
599
+ </label>
600
+ </div>
601
+ <div>
602
+ <label>
603
+ {" "}
604
+ Ruler ticks:{" "}
605
+ <select
606
+ onChange={this.changeRulerTicks}
607
+ value={this.props.rulerTicks}
608
+ >
609
+ {_.map(
610
+ [1, 2, 4, 8, 10, 16],
611
+ function (n) {
612
+ return (
613
+ <option value={n}>
614
+ {n}
615
+ </option>
616
+ );
617
+ },
618
+ )}
619
+ </select>
620
+ </label>
621
+ </div>
622
+ </div>
623
+ )}
624
+ </div>
625
+ )}
626
+ </div>
627
+ );
628
+ },
629
+ });
630
+
631
+ export default GraphSettings;