@khanacademy/perseus-editor 3.0.1 → 4.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.
@@ -1,1007 +0,0 @@
1
- /* eslint-disable camelcase, react/sort-comp */
2
- import {
3
- line as kline,
4
- number as knumber,
5
- point as kpoint,
6
- ray as kray,
7
- vector as kvector,
8
- } from "@khanacademy/kmath";
9
- import {
10
- components,
11
- ApiOptions,
12
- Errors,
13
- KhanColors,
14
- PerseusError,
15
- TransformerWidget,
16
- Util,
17
- } from "@khanacademy/perseus";
18
- import * as React from "react";
19
- import _ from "underscore";
20
-
21
- import GraphSettings from "../components/graph-settings";
22
-
23
- const {Graph, InfoTip, PropCheckBox} = components;
24
- const Transformer = TransformerWidget.widget;
25
- const {deepEq, getGridStep} = Util;
26
-
27
- function arraySum(array) {
28
- return _.reduce(
29
- array,
30
- function (memo, arg) {
31
- return memo + arg;
32
- },
33
- 0,
34
- );
35
- }
36
-
37
- const defaultBackgroundImage = {
38
- url: null,
39
- } as const;
40
-
41
- /* Scales a distance from the default range of
42
- * [-10, 10] to a given props.range pair
43
- *
44
- * Used for sizing various transformation tools
45
- * (rotation handle, dilation circle)
46
- */
47
- function scaleToRange(dist: number, range: any) {
48
- const spreadX = range[0][1] - range[0][0];
49
- const spreadY = range[1][1] - range[1][0];
50
-
51
- return (dist * Math.max(spreadX, spreadY)) / 20;
52
- }
53
-
54
- function orderInsensitiveCoordsEqual(coords1: any, coords2: any) {
55
- coords1 = _.clone(coords1).sort(kpoint.compare);
56
- coords2 = _.clone(coords2).sort(kpoint.compare);
57
- return _.all(
58
- _.map(coords1, function (coord1, i) {
59
- const coord2 = coords2[i];
60
- return kpoint.equal(coord1, coord2);
61
- }),
62
- );
63
- }
64
-
65
- const defaultGraphProps = function (setProps: any, boxSize: number) {
66
- setProps = setProps || {};
67
- const labels = setProps.labels || ["x", "y"];
68
- const range = setProps.range || [
69
- [-10, 10],
70
- [-10, 10],
71
- ];
72
- const step = setProps.step || [1, 1];
73
- const gridStep = setProps.gridStep || getGridStep(range, step, boxSize);
74
- return {
75
- box: [boxSize, boxSize],
76
- labels: labels,
77
- range: range,
78
- step: step,
79
- gridStep: gridStep,
80
- valid: true,
81
- backgroundImage: defaultBackgroundImage,
82
- markings: "grid",
83
- showProtractor: false,
84
- };
85
- };
86
-
87
- const defaultTransformerProps = {
88
- apiOptions: ApiOptions.defaults,
89
- gradeEmpty: false,
90
- graphMode: "interactive",
91
- listMode: "dynamic",
92
- graph: {},
93
- tools: {
94
- translation: {
95
- enabled: true,
96
- required: false,
97
- constraints: {},
98
- },
99
- rotation: {
100
- enabled: true,
101
- required: false,
102
- constraints: {
103
- fixed: false,
104
- },
105
- coord: [1, 6],
106
- },
107
- reflection: {
108
- enabled: true,
109
- required: false,
110
- constraints: {
111
- fixed: false,
112
- },
113
- coords: [
114
- [2, -4],
115
- [2, 2],
116
- ],
117
- },
118
- dilation: {
119
- enabled: true,
120
- required: false,
121
- constraints: {
122
- fixed: false,
123
- },
124
- coord: [6, 6],
125
- },
126
- },
127
- drawSolutionShape: true,
128
- starting: {
129
- shape: {
130
- type: "polygon-3",
131
- coords: [
132
- [2, 2],
133
- [2, 6],
134
- [7, 2],
135
- ],
136
- },
137
- transformations: [],
138
- },
139
- correct: {
140
- shape: {
141
- type: "polygon-3",
142
- coords: [
143
- [2, 2],
144
- [2, 6],
145
- [7, 2],
146
- ],
147
- },
148
- transformations: [],
149
- },
150
- } as const;
151
-
152
- class ToolSettings extends React.Component<any> {
153
- static defaultProps = {
154
- allowFixed: true,
155
- };
156
-
157
- render(): React.ReactNode {
158
- return (
159
- <div>
160
- {this.props.name}:{" "}
161
- <PropCheckBox
162
- label="enabled:"
163
- enabled={this.props.settings.enabled}
164
- onChange={this.props.onChange}
165
- />{" "}
166
- {this.props.settings.enabled && (
167
- <PropCheckBox
168
- label="required:"
169
- required={this.props.settings.required}
170
- onChange={this.props.onChange}
171
- />
172
- )}
173
- {this.props.settings.enabled && (
174
- <InfoTip>
175
- 'Required' will only grade the answer as correct if the
176
- student has used at least one such transformation.
177
- </InfoTip>
178
- )}{" "}
179
- {this.props.allowFixed && this.props.settings.enabled && (
180
- <PropCheckBox
181
- label="fixed:"
182
- fixed={this.props.settings.constraints.fixed}
183
- onChange={this.changeConstraints}
184
- />
185
- )}
186
- {this.props.allowFixed && this.props.settings.enabled && (
187
- <InfoTip>
188
- Enable 'fixed' to prevent the student from repositioning
189
- the tool. The tool will appear in the position at which
190
- it is placed in the editor below.
191
- </InfoTip>
192
- )}
193
- </div>
194
- );
195
- }
196
-
197
- changeConstraints = (changed) => {
198
- const newConstraints = _.extend({}, this.props.constraints, changed);
199
- this.props.onChange({
200
- constraints: newConstraints,
201
- });
202
- };
203
- }
204
-
205
- class TransformationExplorerSettings extends React.Component<any> {
206
- render(): React.ReactNode {
207
- return (
208
- <div className="transformer-settings">
209
- <div>
210
- {" "}
211
- Mode:{" "}
212
- <select value={this.getMode()} onChange={this.changeMode}>
213
- <option value="interactive,dynamic">
214
- {" "}
215
- Exploration with text{" "}
216
- </option>
217
- <option value="interactive,static">
218
- {" "}
219
- Exploration without text{" "}
220
- </option>
221
- <option value="dynamic,interactive">
222
- {" "}
223
- Formal with movement{" "}
224
- </option>
225
- <option value="static,interactive">
226
- {" "}
227
- Formal without movement{" "}
228
- </option>
229
- </select>
230
- <InfoTip>
231
- <ul>
232
- <li>
233
- <b>Exploration:</b> Students create
234
- transformations with tools on the graph.{" "}
235
- </li>
236
- <li>
237
- <b>Formal with movement:</b> Students specify
238
- transformations mathematically in the
239
- transformation list. Graph shows the results of
240
- these transformations.{" "}
241
- </li>
242
- <li>
243
- <b>Formal without movement:</b> Students specify
244
- transformations mathematically in the
245
- transformation list. Graph does not update.{" "}
246
- </li>
247
- </ul>
248
- </InfoTip>
249
- </div>
250
- <ToolSettings
251
- name="Translations"
252
- settings={this.props.tools.translation}
253
- allowFixed={false}
254
- onChange={this.changeHandlerFor("translation")}
255
- />
256
- <ToolSettings
257
- name="Rotations"
258
- settings={this.props.tools.rotation}
259
- onChange={this.changeHandlerFor("rotation")}
260
- />
261
- <ToolSettings
262
- name="Reflections"
263
- settings={this.props.tools.reflection}
264
- onChange={this.changeHandlerFor("reflection")}
265
- />
266
- <ToolSettings
267
- name="Dilations"
268
- settings={this.props.tools.dilation}
269
- onChange={this.changeHandlerFor("dilation")}
270
- />
271
- <PropCheckBox
272
- label="Draw Solution:"
273
- drawSolutionShape={this.props.drawSolutionShape}
274
- onChange={this.props.onChange}
275
- />
276
- </div>
277
- );
278
- }
279
-
280
- getMode = () => {
281
- return this.props.graphMode + "," + this.props.listMode;
282
- };
283
-
284
- changeMode = (e) => {
285
- const selected = e.target.value;
286
- const modes = selected.split(",");
287
-
288
- this.props.onChange({
289
- graphMode: modes[0],
290
- listMode: modes[1],
291
- });
292
- };
293
-
294
- changeHandlerFor = (toolName: string) => {
295
- return (change: any) => {
296
- const newTools = _.clone(this.props.tools);
297
- newTools[toolName] = _.extend(
298
- {},
299
- this.props.tools[toolName],
300
- change,
301
- );
302
-
303
- this.props.onChange({
304
- tools: newTools,
305
- });
306
- };
307
- };
308
- }
309
-
310
- const ShapeTypes: any = {
311
- getPointCountForType: function (type) {
312
- const splitType = type.split("-");
313
- if (splitType[0] === "polygon") {
314
- return splitType[1] || 3;
315
- }
316
- if (splitType[0] === "line" || splitType[0] === "lineSegment") {
317
- return 2;
318
- }
319
- if (splitType[0] === "angle") {
320
- return 3;
321
- }
322
- if (splitType[0] === "circle") {
323
- return 2;
324
- }
325
- if (splitType[0] === "point") {
326
- return 1;
327
- }
328
- },
329
-
330
- addMovableShape: function (graphie, options) {
331
- if (options.editable && options.translatable) {
332
- throw new PerseusError(
333
- "It doesn't make sense to have a movable shape " +
334
- "where you can stretch the points and translate them " +
335
- "simultaneously.",
336
- Errors.InvalidInput,
337
- {metadata: {options}},
338
- );
339
- }
340
-
341
- const points = _.map(options.shape.coords, function (coord) {
342
- let isMoving = false;
343
- let previousCoord = coord;
344
-
345
- const onMove = function (x: any, y) {
346
- if (!isMoving) {
347
- previousCoord = currentPoint.coord;
348
- isMoving = true;
349
- }
350
-
351
- let moveVector = kvector.subtract([x, y], currentPoint.coord);
352
-
353
- // Translate from (x, y) semantics to (dX, dY) semantics
354
- // This is more useful for translations on multiple points,
355
- // where we care about how the points moved, not where any
356
- // individual point ended up
357
- if (options.onMove) {
358
- moveVector = options.onMove(moveVector[0], moveVector[1]);
359
- }
360
-
361
- // Perform a translation on all points in this shape when
362
- // any point moves
363
- if (options.translatable) {
364
- _.each(points, function (point) {
365
- // The point itself will be updated by the
366
- // movablePoint class, so only translate the other
367
- // points
368
- if (point !== currentPoint) {
369
- point.setCoord(
370
- kvector.add(point.coord, moveVector),
371
- );
372
- }
373
- });
374
- }
375
-
376
- // Update our shape and our currentPoint
377
- // Without this, some shapes (circles, angles) appear
378
- // "bouncy" as they are updated with currentPoint at the
379
- // current mouse coordinate (oldCoord), rather than newCoord
380
- const oldCoord = currentPoint.coord;
381
- const newCoord = kvector.add(currentPoint.coord, moveVector);
382
- // Temporarily change our coordinate so that
383
- // shape.update() sees the new coordinate
384
- currentPoint.coord = newCoord;
385
- shape.update();
386
- // ...But don't break onMove, which assumes it
387
- // is the only thing changing our coord
388
- currentPoint.coord = oldCoord;
389
- return newCoord;
390
- };
391
-
392
- const onMoveEnd = function () {
393
- // onMove isn't guaranteed to be called before onMoveEnd, so
394
- // we have to take into account that we may not have moved and
395
- // set previousCoord.
396
- if (options.onMoveEnd && isMoving) {
397
- isMoving = false;
398
- // We don't use the supplied x and y parameters here
399
- // because MovablePoint's onMoveEnd semantics suck.
400
- // It returns the mouseX, mouseY without processing them
401
- // through onMove, leaving us with weird fractional moves
402
- const change = kvector.subtract(
403
- currentPoint.coord,
404
- previousCoord,
405
- );
406
- options.onMoveEnd(change[0], change[1]);
407
- }
408
- shape.update();
409
- };
410
-
411
- const currentPoint = graphie.addMovablePoint({
412
- coord: coord,
413
- normalStyle: options.normalPointStyle,
414
- highlightStyle: options.highlightPointStyle,
415
- constraints: {
416
- fixed: !options.translatable && !options.editable,
417
- },
418
- visible: options.showPoints,
419
- snapX: (options.snap && options.snap[0]) || 0,
420
- snapY: (options.snap && options.snap[1]) || 0,
421
- bounded: false, // Don't bound it when placing it on the graph
422
- onMove: onMove,
423
- onMoveEnd: onMoveEnd,
424
- });
425
-
426
- // Bound it when moving
427
- // We can't set this earlier, because doing so would mean any
428
- // points outside of the graph would be moved into a moved into
429
- // a position that doesn't preserve the shape
430
- currentPoint.bounded = true;
431
-
432
- return currentPoint;
433
- });
434
-
435
- const shape = ShapeTypes.addShape(graphie, options, points);
436
- const removeShapeWithoutPoints = shape.remove;
437
- shape.remove = function () {
438
- removeShapeWithoutPoints.apply(shape);
439
- _.invoke(points, "remove");
440
- };
441
- return shape;
442
- },
443
-
444
- addShape: function (graphie, options, points) {
445
- points = points || options.shape.coords;
446
-
447
- const types = ShapeTypes._typesOf(options.shape);
448
- const typeOptions =
449
- options.shape.options || ShapeTypes.defaultOptions(types);
450
-
451
- const shapes = ShapeTypes._mapTypes(
452
- types,
453
- points,
454
- function (type, points, i) {
455
- const shapeOptions = _.extend({}, options, typeOptions[i]);
456
- return ShapeTypes._addType(graphie, type, points, shapeOptions);
457
- },
458
- );
459
-
460
- const updateFuncs = _.filter(_.pluck(shapes, "update"), _.identity);
461
- const update = function () {
462
- _.invoke(updateFuncs, "call");
463
- };
464
-
465
- const removeFuncs = _.filter(_.pluck(shapes, "remove"), _.identity);
466
- const remove = function () {
467
- _.invoke(removeFuncs, "call");
468
- };
469
-
470
- const getOptions = function () {
471
- return _.map(shapes, function (shape) {
472
- if (shape.getOptions) {
473
- return shape.getOptions();
474
- }
475
- return {};
476
- });
477
- };
478
-
479
- const toJSON = function () {
480
- const coords = _.map(points, function (pt) {
481
- if (_.isArray(pt)) {
482
- return pt;
483
- }
484
- return pt.coord;
485
- });
486
- return {
487
- type: types,
488
- coords: coords,
489
- options: getOptions(),
490
- };
491
- };
492
-
493
- return {
494
- type: types,
495
- points: points,
496
- update: update,
497
- remove: remove,
498
- toJSON: toJSON,
499
- getOptions: getOptions,
500
- };
501
- },
502
-
503
- equal: function (shape1, shape2) {
504
- const types1 = ShapeTypes._typesOf(shape1);
505
- const types2 = ShapeTypes._typesOf(shape2);
506
- if (types1.length !== types2.length) {
507
- return false;
508
- }
509
- const shapes1 = ShapeTypes._mapTypes(
510
- types1,
511
- shape1.coords,
512
- ShapeTypes._combine,
513
- );
514
- const shapes2 = ShapeTypes._mapTypes(
515
- types2,
516
- shape2.coords,
517
- ShapeTypes._combine,
518
- );
519
- return _.all(
520
- _.map(shapes1, function (partialShape1, i) {
521
- const partialShape2 = shapes2[i];
522
- if (partialShape1.type !== partialShape2.type) {
523
- return false;
524
- }
525
- return ShapeTypes._forType(partialShape1.type).equal(
526
- partialShape1.coords,
527
- partialShape2.coords,
528
- );
529
- }),
530
- );
531
- },
532
-
533
- _typesOf: function (shape) {
534
- let types = shape.type;
535
- if (!_.isArray(types)) {
536
- types = [types];
537
- }
538
- return _.map(types, function (type) {
539
- if (type === "polygon") {
540
- return "polygon-3";
541
- }
542
- return type;
543
- });
544
- },
545
-
546
- defaultOptions: function (types) {
547
- return _.map(types, function (type) {
548
- const typeDefaultOptions = ShapeTypes._forType(type).defaultOptions;
549
- return _.extend({}, typeDefaultOptions);
550
- });
551
- },
552
-
553
- _forType: function (type) {
554
- const baseType = type.split("-")[0];
555
- return ShapeTypes[baseType];
556
- },
557
-
558
- _mapTypes: function (types, points, func, context) {
559
- return _.map(types, function (type, i) {
560
- const pointCount = ShapeTypes.getPointCountForType(type);
561
- const currentPoints = _.first(points, pointCount);
562
- points = _.rest(points, pointCount);
563
- return func.call(context, type, currentPoints, i);
564
- });
565
- },
566
-
567
- _addType: function (graphie, type, points, options) {
568
- const lineCoords = _.isArray(points[0])
569
- ? {
570
- coordA: points[0],
571
- coordZ: points[1],
572
- }
573
- : {
574
- pointA: points[0],
575
- pointZ: points[1],
576
- };
577
-
578
- type = type.split("-")[0];
579
- if (type === "polygon") {
580
- const polygon = graphie.addMovablePolygon(
581
- _.extend({}, options, {
582
- fixed: !options.editable,
583
- snapX: (options.snap && options.snap[0]) || 0,
584
- snapY: (options.snap && options.snap[1]) || 0,
585
- points: points,
586
- constrainToGraph: false,
587
- }),
588
- );
589
- return {
590
- update: polygon.transform.bind(polygon),
591
- remove: polygon.remove.bind(polygon),
592
- };
593
- }
594
- if (type === "line" || type === "lineSegment") {
595
- const line = graphie.addMovableLineSegment(
596
- _.extend({}, options, lineCoords, {
597
- movePointsWithLine: true,
598
- fixed: true,
599
- constraints: {
600
- fixed: true,
601
- },
602
- extendLine: type === "line",
603
- }),
604
- );
605
-
606
- // TODO(jack): Hide points on uneditable lines when translation
607
- // is a vector.
608
- // We can't just remove the points yet, because they are the
609
- // translation handle for the line.
610
- return {
611
- update: line.transform.bind(line, true),
612
- remove: line.remove.bind(line),
613
- };
614
- }
615
- if (type === "angle") {
616
- // If this angle is editable, we want to be able to make angles
617
- // both larger and smaller than 180 degrees.
618
- // If this angle is not editable, it should always maintain
619
- // it's angle measure, even if it is reflected (causing the
620
- // clockwise-ness of the points to change)
621
- const shouldChangeReflexivity = options.editable ? null : false;
622
-
623
- const angle = graphie.addMovableAngle({
624
- angleLabel: "$deg0",
625
- fixed: true,
626
- points: points,
627
- normalStyle: options.normalStyle,
628
- reflex: options.reflex,
629
- });
630
-
631
- // Hide non-vertex points on uneditable angles
632
- if (!_.isArray(points[0]) && !options.editable) {
633
- points[0].remove();
634
- points[2].remove();
635
- }
636
- return {
637
- update: angle.update.bind(angle, shouldChangeReflexivity),
638
- remove: angle.remove.bind(angle),
639
- getOptions: function () {
640
- return {
641
- reflex: angle.isReflex(),
642
- };
643
- },
644
- };
645
- }
646
- if (type === "circle") {
647
- let perimeter = {
648
- // temporary object for the first removal
649
- remove: _.identity,
650
- };
651
- const redrawPerim = function () {
652
- const coord0 = points[0].coord || points[0];
653
- const coord1 = points[1].coord || points[1];
654
- const radius = kpoint.distanceToPoint(coord0, coord1);
655
- // @ts-expect-error - TS2554 - Expected 1 arguments, but got 0.
656
- perimeter.remove();
657
- perimeter = graphie.circle(
658
- coord0,
659
- radius,
660
- _.extend(
661
- {
662
- stroke: KhanColors.DYNAMIC,
663
- "stroke-width": 2,
664
- },
665
- options.normalStyle,
666
- ),
667
- );
668
- };
669
-
670
- redrawPerim();
671
- if (points[1].remove && !options.editable) {
672
- points[1].remove();
673
- }
674
-
675
- return {
676
- update: redrawPerim,
677
- remove: function () {
678
- // Not _.bind because the remove function changes
679
- // when the perimeter is redrawn
680
- // @ts-expect-error - TS2554 - Expected 1 arguments, but got 0.
681
- perimeter.remove();
682
- },
683
- };
684
- }
685
- if (type === "point") {
686
- // do nothing
687
- return {
688
- update: null,
689
- remove: null,
690
- };
691
- }
692
- throw new PerseusError(
693
- "Invalid shape type " + type,
694
- Errors.InvalidInput,
695
- );
696
- },
697
-
698
- _combine: function (type, coords) {
699
- return {
700
- type: type,
701
- coords: coords,
702
- };
703
- },
704
-
705
- polygon: {
706
- equal: orderInsensitiveCoordsEqual,
707
- },
708
-
709
- line: {
710
- equal: kline.equal,
711
- },
712
-
713
- lineSegment: {
714
- equal: orderInsensitiveCoordsEqual,
715
- },
716
-
717
- angle: {
718
- equal: function (points1, points2) {
719
- if (!kpoint.equal(points1[1], points2[1])) {
720
- return false;
721
- }
722
-
723
- const line1_0 = [points1[1], points1[0]];
724
- const line1_2 = [points1[1], points1[2]];
725
- const line2_0 = [points2[1], points2[0]];
726
- const line2_2 = [points2[1], points2[2]];
727
-
728
- const equalUnflipped =
729
- // @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'. | TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'.
730
- kray.equal(line1_0, line2_0) && kray.equal(line1_2, line2_2);
731
- const equalFlipped =
732
- // @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'. | TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'.
733
- kray.equal(line1_0, line2_2) && kray.equal(line1_2, line2_0);
734
-
735
- return equalUnflipped || equalFlipped;
736
- },
737
-
738
- defaultOptions: {
739
- reflex: false,
740
- },
741
- },
742
-
743
- circle: {
744
- equal: function (points1, points2) {
745
- const radius1 = kpoint.distanceToPoint(points1[0], points1[1]);
746
- const radius2 = kpoint.distanceToPoint(points2[0], points2[1]);
747
- return (
748
- kpoint.equal(points1[0], points2[0]) &&
749
- knumber.equal(radius1, radius2)
750
- );
751
- },
752
- },
753
-
754
- point: {
755
- equal: kpoint.equal,
756
- },
757
- };
758
-
759
- class TransformationsShapeEditor extends React.Component<any> {
760
- render(): React.ReactNode {
761
- return (
762
- <div>
763
- <Graph
764
- // eslint-disable-next-line react/no-string-refs
765
- ref="graph"
766
- box={this.props.graph.box}
767
- range={this.props.graph.range}
768
- labels={this.props.graph.labels}
769
- step={this.props.graph.step}
770
- gridStep={this.props.graph.gridStep}
771
- markings={this.props.graph.markings}
772
- backgroundImage={this.props.graph.backgroundImage}
773
- onGraphieUpdated={this.setupGraphie}
774
- />
775
- <select
776
- key="type-select"
777
- value={this.getTypeString(this.props.shape.type)}
778
- onChange={this.changeType}
779
- >
780
- <option value="polygon-3">Triangle</option>
781
- <option value="polygon-4">Quadrilateral</option>
782
- <option value="polygon-5">Pentagon</option>
783
- <option value="polygon-6">Hexagon</option>
784
- <option value="line">Line</option>
785
- <option value="line,line">2 lines</option>
786
- <option value="lineSegment">Line segment</option>
787
- <option value="lineSegment,lineSegment">
788
- {" "}
789
- 2 line segments{" "}
790
- </option>
791
- <option value="angle">Angle</option>
792
- <option value="circle">Circle</option>
793
- </select>
794
- </div>
795
- );
796
- }
797
-
798
- /* Return the option string for a given type */
799
- getTypeString = (type: any) => {
800
- if (_.isArray(type)) {
801
- return _.map(type, this.getTypeString).join(",");
802
- }
803
- if (type === "polygon") {
804
- return "polygon-" + this.props.shape.coords.length;
805
- }
806
- return type;
807
- };
808
-
809
- /* Change the type on the window event e
810
- *
811
- * e.target.value is the new type string
812
- */
813
- changeType = (e: any) => {
814
- const types = String(e.target.value).split(",");
815
- const pointCount = arraySum(
816
- _.map(types, ShapeTypes.getPointCountForType),
817
- );
818
-
819
- // eslint-disable-next-line react/no-string-refs
820
- // @ts-expect-error - TS2339 - Property 'props' does not exist on type 'ReactInstance'.
821
- const radius = scaleToRange(4, this.refs.graph.props.range);
822
- const offset = (1 / 2 - 1 / pointCount) * 180;
823
- const coords = _.times(pointCount, function (i) {
824
- return kpoint.rotateDeg(
825
- [radius, 0],
826
- (360 * i) / pointCount + offset,
827
- );
828
- });
829
-
830
- this.props.onChange({
831
- shape: {
832
- type: types,
833
- coords: coords,
834
- options: ShapeTypes.defaultOptions(types),
835
- },
836
- });
837
- };
838
-
839
- componentDidMount() {
840
- // eslint-disable-next-line react/no-string-refs
841
- // @ts-expect-error - TS2339 - Property 'graphie' does not exist on type 'ReactInstance'.
842
- this.setupGraphie(this.refs.graph.graphie());
843
- }
844
-
845
- componentDidUpdate(prevProps) {
846
- if (!deepEq(prevProps.shape, this.props.shape)) {
847
- // eslint-disable-next-line react/no-string-refs
848
- // @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'.
849
- this.refs.graph.reset();
850
- }
851
- }
852
-
853
- updateCoords = () => {
854
- this.props.onChange({
855
- // @ts-expect-error - TS2339 - Property 'shape' does not exist on type 'TransformationsShapeEditor'.
856
- shape: this.shape.toJSON(),
857
- });
858
- };
859
-
860
- setupGraphie = (graphie: any) => {
861
- // @ts-expect-error - TS2339 - Property 'shape' does not exist on type 'TransformationsShapeEditor'.
862
- this.shape = ShapeTypes.addMovableShape(graphie, {
863
- editable: true,
864
- snap: graphie.snap,
865
- shape: this.props.shape,
866
- onMoveEnd: this.updateCoords,
867
- });
868
- };
869
- }
870
-
871
- class TransformerEditor extends React.Component<any> {
872
- // TODO (jack): These should be refactored into a nice object at the top
873
- // so that we don't have all this duplication
874
- static defaultProps: any = defaultTransformerProps;
875
-
876
- static widgetName = "transformer" as const;
877
-
878
- render(): React.ReactNode {
879
- // Fill in any missing value in this.props.graph
880
- // this can happen because the graph json doesn't include
881
- // box, for example
882
- const graph = _.extend(
883
- defaultGraphProps(this.props.graph, 340),
884
- this.props.graph,
885
- );
886
-
887
- return (
888
- <div>
889
- <div>
890
- <PropCheckBox
891
- label="Grade empty answers as wrong:"
892
- gradeEmpty={this.props.gradeEmpty}
893
- onChange={this.props.onChange}
894
- />
895
- <InfoTip>
896
- <p>
897
- We generally do not grade empty answers. This
898
- usually works well, but sometimes can result in
899
- giving away part of an answer in a multi-part
900
- question.
901
- </p>
902
- <p>
903
- If this is a multi-part question (there is another
904
- widget), you probably want to enable this option.
905
- Otherwise, you should leave it disabled.
906
- </p>
907
- <p>Confused? Talk to Elizabeth.</p>
908
- </InfoTip>
909
- </div>
910
- <div>Graph settings:</div>
911
- <GraphSettings
912
- box={graph.box}
913
- labels={graph.labels}
914
- range={graph.range}
915
- step={graph.step}
916
- gridStep={graph.gridStep}
917
- valid={graph.valid}
918
- backgroundImage={graph.backgroundImage}
919
- markings={graph.markings}
920
- showProtractor={graph.showProtractor}
921
- onChange={this.changeGraph}
922
- />
923
- <div>Transformation settings:</div>
924
- <TransformationExplorerSettings
925
- // eslint-disable-next-line react/no-string-refs
926
- ref="transformationSettings"
927
- graphMode={this.props.graphMode}
928
- listMode={this.props.listMode}
929
- tools={this.props.tools}
930
- drawSolutionShape={this.props.drawSolutionShape}
931
- onChange={this.props.onChange}
932
- />
933
- <div>Starting location:</div>
934
- <TransformationsShapeEditor
935
- // eslint-disable-next-line react/no-string-refs
936
- ref="shapeEditor"
937
- graph={graph}
938
- shape={this.props.starting.shape}
939
- onChange={this.changeStarting}
940
- setDrawingAreaAvailable={
941
- this.props.apiOptions.setDrawingAreaAvailable
942
- }
943
- />
944
- <div>Solution transformations:</div>
945
- <Transformer
946
- // eslint-disable-next-line react/no-string-refs
947
- ref="explorer"
948
- graph={graph}
949
- graphMode={this.props.graphMode}
950
- listMode={this.props.listMode}
951
- gradeEmpty={this.props.gradeEmpty}
952
- tools={this.props.tools}
953
- drawSolutionShape={this.props.drawSolutionShape}
954
- starting={this.props.starting}
955
- correct={this.props.starting}
956
- transformations={this.props.correct.transformations}
957
- onChange={this.changeTransformer}
958
- trackInteraction={() => {}}
959
- />
960
- </div>
961
- );
962
- }
963
-
964
- // propagate a props change on our graph settings to
965
- // this.props.graph
966
- changeGraph: (arg1: any, arg2: any) => void = (graphChanges, callback) => {
967
- const newGraph = _.extend({}, this.props.graph, graphChanges);
968
- this.props.onChange(
969
- {
970
- graph: newGraph,
971
- },
972
- callback,
973
- );
974
- };
975
-
976
- // propagate a props change on our starting graph to
977
- // this.props.starting
978
- changeStarting: (arg1: any) => void = (startingChanges) => {
979
- const newStarting = _.extend({}, this.props.starting, startingChanges);
980
- this.props.onChange({
981
- starting: newStarting,
982
- });
983
- };
984
-
985
- // propagate a transformations change onto correct.transformations
986
- changeTransformer: (arg1: any, arg2: any) => void = (changes, callback) => {
987
- if (changes.transformations) {
988
- changes.correct = {
989
- ...this.props.correct,
990
- transformations: changes.transformations,
991
- };
992
- delete changes.transformations;
993
- }
994
- this.props.onChange(changes, callback);
995
- };
996
-
997
- serialize: () => any = () => {
998
- // eslint-disable-next-line react/no-string-refs
999
- // @ts-expect-error - TS2339 - Property 'getEditorJSON' does not exist on type 'ReactInstance'.
1000
- const json = this.refs.explorer.getEditorJSON();
1001
- json.correct = json.answer;
1002
- delete json.answer;
1003
- return json;
1004
- };
1005
- }
1006
-
1007
- export default TransformerEditor;