@khanacademy/perseus 20.5.0 → 20.7.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 (35) hide show
  1. package/dist/components/icon.d.ts +1 -0
  2. package/dist/es/index.css +19 -6
  3. package/dist/es/index.js +750 -269
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +19 -6
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +853 -358
  8. package/dist/index.js.map +1 -1
  9. package/dist/interactive2/movable.d.ts +1 -1
  10. package/dist/interactive2/objective_.d.ts +2 -2
  11. package/dist/styles/global-styles.d.ts +1 -0
  12. package/dist/styles/shared.d.ts +1 -0
  13. package/dist/types.d.ts +102 -0
  14. package/dist/util/a11y.d.ts +1 -0
  15. package/dist/util/extract-perseus-data.d.ts +8 -1
  16. package/dist/util/snowman-utils.d.ts +90 -0
  17. package/dist/util/widget-enum-utils.d.ts +11 -0
  18. package/dist/util.d.ts +1 -1
  19. package/dist/widgets/interactive-graph.d.ts +9 -2
  20. package/dist/widgets/interactive-graphs/graphs/index.d.ts +1 -0
  21. package/dist/widgets/interactive-graphs/graphs/polygon.d.ts +5 -0
  22. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +1 -1
  23. package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +27 -0
  24. package/dist/widgets/interactive-graphs/reducer/interactive-graph-reducer.d.ts +3 -0
  25. package/dist/widgets/interactive-graphs/{interactive-graph-state.d.ts → reducer/interactive-graph-state.d.ts} +2 -2
  26. package/dist/widgets/interactive-graphs/types.d.ts +12 -6
  27. package/dist/widgets/interactive-graphs/utils.d.ts +5 -0
  28. package/dist/widgets/label-image/answer-pill.d.ts +1 -0
  29. package/dist/widgets/label-image/marker.d.ts +1 -0
  30. package/dist/widgets/radio/choice-icon/shared-styles.d.ts +1 -0
  31. package/dist/widgets/radio.d.ts +1 -1
  32. package/package.json +4 -6
  33. package/dist/widgets/interactive-graphs/interactive-graph-action.d.ts +0 -17
  34. package/dist/widgets/interactive-graphs/interactive-graph-reducer.d.ts +0 -3
  35. /package/dist/widgets/radio/{widget.d.ts → radio.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -18894,7 +18894,7 @@ const isAccessible = widgetInfo => {
18894
18894
  }
18895
18895
  return !!accessible;
18896
18896
  };
18897
- const getAllWidgetTypes = () => {
18897
+ const getAllWidgetTypes$1 = () => {
18898
18898
  return ___default["default"].keys(widgets);
18899
18899
  };
18900
18900
  const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
@@ -19166,7 +19166,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19166
19166
  getVersionVector: getVersionVector,
19167
19167
  getPublicWidgets: getPublicWidgets,
19168
19168
  isAccessible: isAccessible,
19169
- getAllWidgetTypes: getAllWidgetTypes,
19169
+ getAllWidgetTypes: getAllWidgetTypes$1,
19170
19170
  upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
19171
19171
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
19172
19172
  traverseChildWidgets: traverseChildWidgets$3,
@@ -19560,7 +19560,6 @@ class Renderer extends React__namespace.Component {
19560
19560
 
19561
19561
  // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19562
19562
  _getAllWidgetsInfo = props => {
19563
- // @ts-expect-error - TS2345 - Argument of type '{ [key: string]: PerseusWidget; }' is not assignable to parameter of type 'Partial<Record<string, CategorizerWidget>>'.
19564
19563
  return mapObject$1(props.widgets, (widgetInfo, widgetId) => {
19565
19564
  if (!widgetInfo.type || !widgetInfo.alignment) {
19566
19565
  const newValues = {};
@@ -28321,22 +28320,55 @@ const GraphLockedLayer = props => {
28321
28320
  };
28322
28321
 
28323
28322
  const MOVE_CONTROL_POINT = "move-control-point";
28324
- function moveControlPoint(objectIndex, pointIndex, destination) {
28323
+ function moveControlPoint(pointIndex, destination, itemIndex) {
28325
28324
  return {
28326
28325
  type: MOVE_CONTROL_POINT,
28327
- objectIndex,
28326
+ itemIndex,
28328
28327
  pointIndex,
28329
28328
  destination
28330
28329
  };
28331
28330
  }
28331
+ const MOVE_ALL = "move-all";
28332
28332
  const MOVE_LINE = "move-line";
28333
- function moveSegment(lineIndex, delta) {
28333
+ /** This action assumes the state.coords holds an array of collinear tuples that define lines */
28334
+ function moveLine(itemIndex, delta) {
28334
28335
  return {
28335
28336
  type: MOVE_LINE,
28336
- lineIndex,
28337
+ itemIndex,
28337
28338
  delta
28338
28339
  };
28339
28340
  }
28341
+ /** This action assumes a flat array of vectors */
28342
+ const moveAll = delta => ({
28343
+ type: MOVE_ALL,
28344
+ delta
28345
+ });
28346
+
28347
+ /**
28348
+ * 44 is touch best practice and AAA compliant for WCAG
28349
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
28350
+ */
28351
+ const TARGET_SIZE = 44;
28352
+
28353
+ // same as pointsFromNormalized in interactive-graph.tsx
28354
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
28355
+ // maps over [x, y]
28356
+ coords.map((coord, i) => {
28357
+ const axisRange = range[i];
28358
+ if (noSnap) {
28359
+ return axisRange[0] + (axisRange[1] - axisRange[0]) * coord;
28360
+ }
28361
+ const axisStep = step[i];
28362
+ const nSteps = Math.floor((axisRange[1] - axisRange[0]) / axisStep);
28363
+ const tick = Math.round(coord * nSteps);
28364
+ return axisRange[0] + axisStep * tick;
28365
+ }));
28366
+
28367
+ // same as normalizeCoords in interactive-graph.tsx
28368
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
28369
+ const extent = ranges[i][1] - ranges[i][0];
28370
+ return (coord + ranges[i][1]) / extent;
28371
+ }));
28340
28372
 
28341
28373
  const defaultStroke = "var(--movable-line-stroke-color)";
28342
28374
  const MovableLine = props => {
@@ -28384,7 +28416,7 @@ const MovableLine = props => {
28384
28416
  end: endPtPx,
28385
28417
  style: {
28386
28418
  stroke: "transparent",
28387
- strokeWidth: 44
28419
+ strokeWidth: TARGET_SIZE
28388
28420
  }
28389
28421
  }), /*#__PURE__*/React__namespace.createElement(SVGLine, {
28390
28422
  start: startPtPx,
@@ -28417,7 +28449,7 @@ function SVGLine(props) {
28417
28449
  x2: end[0],
28418
28450
  y2: end[1],
28419
28451
  style: style,
28420
- className: dragging ? "movable-line-dragging" : undefined
28452
+ className: dragging ? "movable-dragging" : undefined
28421
28453
  });
28422
28454
  }
28423
28455
 
@@ -28492,7 +28524,7 @@ const StyledMovablePoint = props => {
28492
28524
  }
28493
28525
  }, /*#__PURE__*/React__namespace.createElement("circle", {
28494
28526
  className: "movable-point-hitbox",
28495
- r: 22,
28527
+ r: TARGET_SIZE / 2,
28496
28528
  cx: x,
28497
28529
  cy: y
28498
28530
  }), /*#__PURE__*/React__namespace.createElement("circle", {
@@ -28528,9 +28560,9 @@ const SegmentGraph = props => {
28528
28560
  snaps: snapStep,
28529
28561
  range: range,
28530
28562
  onMoveLine: delta => {
28531
- dispatch(moveSegment(i, delta));
28563
+ dispatch(moveLine(i, delta));
28532
28564
  },
28533
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(i, endpointIndex, destination)),
28565
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28534
28566
  "data-testid": "segment" + i
28535
28567
  })));
28536
28568
  };
@@ -28573,9 +28605,9 @@ const LinearGraph = props => {
28573
28605
  snaps: snapStep,
28574
28606
  range: range,
28575
28607
  onMoveLine: delta => {
28576
- dispatch(moveSegment(i, delta));
28608
+ dispatch(moveLine(i, delta));
28577
28609
  },
28578
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(i, endpointIndex, destination))
28610
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i))
28579
28611
  // "linear" or "linear-system" + index
28580
28612
  ,
28581
28613
  "data-testid": type + i,
@@ -28626,8 +28658,8 @@ const RayGraph = props => {
28626
28658
 
28627
28659
  // a ray only has one line
28628
28660
  const [start, end] = lines[0];
28629
- const handleMoveLine = delta => dispatch(moveSegment(0, delta));
28630
- const handleMovePoint = (newPoint, pointIndex) => dispatch(moveControlPoint(0, pointIndex, newPoint));
28661
+ const handleMoveLine = delta => dispatch(moveLine(0, delta));
28662
+ const handleMovePoint = (newPoint, pointIndex) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
28631
28663
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableLine, {
28632
28664
  start: start,
28633
28665
  end: end,
@@ -28646,6 +28678,62 @@ const RayGraph = props => {
28646
28678
  }));
28647
28679
  };
28648
28680
 
28681
+ const PolygonGraph = props => {
28682
+ const [focused, setFocused] = React__namespace.useState(false);
28683
+ const [hovered, setHovered] = React__namespace.useState(false);
28684
+ const {
28685
+ dispatch
28686
+ } = props;
28687
+ const {
28688
+ coords,
28689
+ type
28690
+ } = props.graphState;
28691
+ const points = coords ?? [[0, 0]];
28692
+ const pointsSum = points.reduce((acc, point) => mafs.vec.add(acc, point), [0, 0]);
28693
+ const midpoint = pointsSum && mafs.vec.scale(pointsSum, 1 / (points.length ?? 1));
28694
+ const ref = React__namespace.useRef(null);
28695
+ const {
28696
+ dragging
28697
+ } = mafs.useMovable({
28698
+ gestureTarget: ref,
28699
+ point: midpoint,
28700
+ onMove: newPoint => {
28701
+ const delta = mafs.vec.sub(newPoint, midpoint);
28702
+ dispatch(moveAll(delta));
28703
+ },
28704
+ constrain: p => p
28705
+ });
28706
+ const active = hovered || focused || dragging;
28707
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28708
+ points: [...points],
28709
+ color: "var(--movable-line-stroke-color)",
28710
+ svgPolygonProps: {
28711
+ strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)"
28712
+ }
28713
+ }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28714
+ points: [...points],
28715
+ color: "transparent",
28716
+ svgPolygonProps: {
28717
+ ref,
28718
+ tabIndex: 0,
28719
+ strokeWidth: TARGET_SIZE,
28720
+ style: {
28721
+ cursor: dragging ? "grabbing" : "grab"
28722
+ },
28723
+ onFocus: () => setFocused(true),
28724
+ onBlur: () => setFocused(false),
28725
+ onMouseEnter: () => setHovered(true),
28726
+ onMouseLeave: () => setHovered(false),
28727
+ className: "movable-polygon"
28728
+ }
28729
+ }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
28730
+ key: i,
28731
+ point: point,
28732
+ onMove: destination => dispatch(moveControlPoint(i, destination)),
28733
+ "data-testid": type + i
28734
+ })));
28735
+ };
28736
+
28649
28737
  const renderLineLabel = (n, _ref) => {
28650
28738
  let [min, max] = _ref;
28651
28739
  return n !== -1 && n !== min && n !== max;
@@ -28661,234 +28749,6 @@ const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React__nam
28661
28749
  yAxis: axisOptions(props, 1)
28662
28750
  });
28663
28751
 
28664
- function interactiveGraphReducer(state, action) {
28665
- switch (action.type) {
28666
- case MOVE_CONTROL_POINT:
28667
- {
28668
- const newCoords = updateAtIndex({
28669
- array: state.coords,
28670
- index: action.objectIndex,
28671
- update: tuple => setAtIndex({
28672
- array: tuple,
28673
- index: action.pointIndex,
28674
- newValue: snap(state, bound$1(state, action.destination))
28675
- })
28676
- });
28677
- if (!validSegments(newCoords)) {
28678
- return state;
28679
- }
28680
- return {
28681
- ...state,
28682
- hasBeenInteractedWith: true,
28683
- coords: newCoords
28684
- };
28685
- }
28686
- case MOVE_LINE:
28687
- {
28688
- const currentLine = state.coords?.[action.lineIndex];
28689
- if (!currentLine) {
28690
- throw new Error("No line to move");
28691
- }
28692
- const maxMoves = currentLine.map(point => maxMove(state, point));
28693
- const minMoves = currentLine.map(point => minMove(state, point));
28694
- const maxXMove = Math.min(...maxMoves.map(move => move[0]));
28695
- const maxYMove = Math.min(...maxMoves.map(move => move[1]));
28696
- const minXMove = Math.max(...minMoves.map(move => move[0]));
28697
- const minYMove = Math.max(...minMoves.map(move => move[1]));
28698
- const dx = clamp(action.delta[0], minXMove, maxXMove);
28699
- const dy = clamp(action.delta[1], minYMove, maxYMove);
28700
- const newValue = [snap(state, mafs.vec.add(currentLine[0], [dx, dy])), snap(state, mafs.vec.add(currentLine[1], [dx, dy]))];
28701
- const newLine = setAtIndex({
28702
- array: state.coords,
28703
- index: action.lineIndex,
28704
- newValue
28705
- });
28706
- return {
28707
- ...state,
28708
- hasBeenInteractedWith: true,
28709
- coords: newLine
28710
- };
28711
- }
28712
- default:
28713
- throw new wonderStuffCore.UnreachableCaseError(action);
28714
- }
28715
- }
28716
- function snap(state, point) {
28717
- const [requestedX, requestedY] = point;
28718
- const [snapX, snapY] = state.snapStep;
28719
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28720
- }
28721
-
28722
- // Returns the closest point to the given `point` that is within the graph
28723
- // bounds given in `state`.
28724
- function bound$1(state, point) {
28725
- const [requestedX, requestedY] = point;
28726
- const [snapX, snapY] = state.snapStep;
28727
- const [[minX, maxX], [minY, maxY]] = state.range;
28728
- return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
28729
- }
28730
-
28731
- // Returns the vector from the given point to the top-right corner of the graph
28732
- function maxMove(state, point) {
28733
- const topRight = bound$1(state, [Infinity, Infinity]);
28734
- return mafs.vec.sub(topRight, point);
28735
- }
28736
-
28737
- // Returns the vector from the given point to the bottom-left corner of the
28738
- // graph
28739
- function minMove(state, point) {
28740
- const bottomLeft = bound$1(state, [-Infinity, -Infinity]);
28741
- return mafs.vec.sub(bottomLeft, point);
28742
- }
28743
- function clamp(value, min, max) {
28744
- if (value < min) {
28745
- return min;
28746
- }
28747
- if (value > max) {
28748
- return max;
28749
- }
28750
- return value;
28751
- }
28752
- function validSegments(segments) {
28753
- return segments.every(_ref => {
28754
- let [start, end] = _ref;
28755
- return !kmath.vector.equal(start, end);
28756
- });
28757
- }
28758
- function updateAtIndex(args) {
28759
- const {
28760
- array = [],
28761
- index,
28762
- update
28763
- } = args;
28764
- const newValue = update(array[index]);
28765
- return setAtIndex({
28766
- array,
28767
- index,
28768
- newValue
28769
- });
28770
- }
28771
- function setAtIndex(args) {
28772
- const {
28773
- array = [],
28774
- index,
28775
- newValue
28776
- } = args;
28777
- const copy = [...array];
28778
- copy[index] = newValue;
28779
- // restoring readonly to array
28780
- return copy;
28781
- }
28782
-
28783
- // same as pointsFromNormalized in interactive-graph.tsx
28784
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
28785
- // maps over [x, y]
28786
- coords.map((coord, i) => {
28787
- const axisRange = range[i];
28788
- if (noSnap) {
28789
- return axisRange[0] + (axisRange[1] - axisRange[0]) * coord;
28790
- }
28791
- const axisStep = step[i];
28792
- const nSteps = Math.floor((axisRange[1] - axisRange[0]) / axisStep);
28793
- const tick = Math.round(coord * nSteps);
28794
- return axisRange[0] + axisStep * tick;
28795
- }));
28796
-
28797
- // same as normalizeCoords in interactive-graph.tsx
28798
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
28799
- const extent = ranges[i][1] - ranges[i][0];
28800
- return (coord + ranges[i][1]) / extent;
28801
- }));
28802
-
28803
- function initializeGraphState(params) {
28804
- const {
28805
- graph,
28806
- step,
28807
- snapStep,
28808
- range
28809
- } = params;
28810
- switch (graph.type) {
28811
- case "segment":
28812
- return {
28813
- type: "segment",
28814
- hasBeenInteractedWith: false,
28815
- range,
28816
- snapStep,
28817
- coords: getDefaultSegments({
28818
- graph,
28819
- step,
28820
- range
28821
- })
28822
- };
28823
- case "linear":
28824
- case "linear-system":
28825
- case "ray":
28826
- return {
28827
- type: graph.type,
28828
- hasBeenInteractedWith: false,
28829
- range,
28830
- snapStep,
28831
- // Linear and ray graphs have a single tuple of points, while a
28832
- // linear system has two tuples of points.
28833
- coords: getLineCoords(graph, range, step)
28834
- };
28835
- }
28836
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
28837
- }
28838
- const getDefaultSegments = props => {
28839
- const ys = n => {
28840
- switch (n) {
28841
- case 2:
28842
- return [5, -5];
28843
- case 3:
28844
- return [5, 0, -5];
28845
- case 4:
28846
- return [6, 2, -2, -6];
28847
- case 5:
28848
- return [6, 3, 0, -3, -6];
28849
- case 6:
28850
- return [5, 3, 1, -1, -3, -5];
28851
- default:
28852
- return [5];
28853
- }
28854
- };
28855
- return ys(props.graph.numSegments).map(y => {
28856
- let endpoints = [[-5, y], [5, y]];
28857
- endpoints = normalizeCoords(endpoints, props.range);
28858
- endpoints = normalizePoints(props.range, props.step, endpoints);
28859
- return endpoints;
28860
- });
28861
- };
28862
-
28863
- // TS v4 doesn't narrow return types, while v5 does.
28864
- // Instead of updating to v5, using generic type to relate input and output types.
28865
- function getGradableGraph(state, initialGraph) {
28866
- if (!state.hasBeenInteractedWith) {
28867
- return initialGraph;
28868
- }
28869
- switch (true) {
28870
- // coords: Array of CollinearTuple
28871
- case state.type === "linear-system" && initialGraph.type === "linear-system":
28872
- case state.type === "segment" && initialGraph.type === "segment":
28873
- return {
28874
- ...initialGraph,
28875
- coords: state.coords
28876
- };
28877
- // coords: CollinearTuple
28878
- case state.type === "linear" && initialGraph.type === "linear":
28879
- case state.type === "ray" && initialGraph.type === "ray":
28880
- return {
28881
- ...initialGraph,
28882
- coords: state.coords?.[0]
28883
- };
28884
- }
28885
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
28886
- }
28887
- const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
28888
- const getLineCoords = (graph, range, step) =>
28889
- // Return two lines for a linear system, one for a ray or linear
28890
- graph.coords ?? graph.type === "linear-system" ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])];
28891
-
28892
28752
  // Displays a stylized open eye: lint warnings are visible
28893
28753
  const VisibleIcon = () => /*#__PURE__*/React__namespace.createElement("svg", {
28894
28754
  width: "24",
@@ -29478,107 +29338,486 @@ const styles$h = aphrodite.StyleSheet.create({
29478
29338
  flexDirection: "row",
29479
29339
  alignItems: "center"
29480
29340
  }
29481
- });
29482
-
29483
- const truth = () => true;
29341
+ });
29342
+
29343
+ const truth = () => true;
29344
+
29345
+ /* A minor abstraction on top of NumberInput for ranges
29346
+ *
29347
+ */
29348
+ class RangeInput extends React__namespace.Component {
29349
+ static propTypes = {
29350
+ value: PropTypes__default["default"].array.isRequired,
29351
+ onChange: PropTypes__default["default"].func.isRequired,
29352
+ placeholder: PropTypes__default["default"].array,
29353
+ checkValidity: PropTypes__default["default"].func
29354
+ };
29355
+ static defaultProps = {
29356
+ placeholder: [null, null]
29357
+ };
29358
+ render() {
29359
+ const value = this.props.value;
29360
+ const checkValidity = this.props.checkValidity || truth;
29361
+ return /*#__PURE__*/React__namespace.createElement("div", {
29362
+ className: "range-input"
29363
+ }, /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29364
+ value: value[0],
29365
+ checkValidity: val => checkValidity([val, value[1]])
29366
+ // eslint-disable-next-line react/jsx-no-bind
29367
+ ,
29368
+ onChange: this.onChange.bind(this, 0),
29369
+ placeholder: this.props.placeholder[0]
29370
+ })), /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29371
+ value: value[1],
29372
+ checkValidity: val => checkValidity([value[0], val])
29373
+ // eslint-disable-next-line react/jsx-no-bind
29374
+ ,
29375
+ onChange: this.onChange.bind(this, 1),
29376
+ placeholder: this.props.placeholder[1]
29377
+ })));
29378
+ }
29379
+ onChange = (i, newVal) => {
29380
+ const value = this.props.value;
29381
+ if (i === 0) {
29382
+ this.props.onChange([newVal, value[1]]);
29383
+ } else {
29384
+ this.props.onChange([value[0], newVal]);
29385
+ }
29386
+ };
29387
+ }
29388
+
29389
+ var components = /*#__PURE__*/Object.freeze({
29390
+ __proto__: null,
29391
+ ButtonGroup: ButtonGroup,
29392
+ Graph: Graph,
29393
+ Graphie: Graphie,
29394
+ HUD: HUD,
29395
+ Icon: Icon,
29396
+ InfoTip: InfoTip,
29397
+ InlineIcon: InlineIcon,
29398
+ MathInput: MathInput,
29399
+ MultiButtonGroup: MultiButtonGroup,
29400
+ NumberInput: NumberInput,
29401
+ PropCheckBox: PropCheckBox,
29402
+ RangeInput: RangeInput,
29403
+ SvgImage: SvgImage,
29404
+ TextInput: TextInput,
29405
+ TextListEditor: TextListEditor
29406
+ });
29407
+
29408
+ /**
29409
+ * If a graphie URL is provided in `backgroundImage`, will return the rendered graphie background.
29410
+ * Otherwise, returns `null`.
29411
+ */
29412
+ const getLegacyGrid = (box, backgroundImage) => {
29413
+ const {
29414
+ url,
29415
+ width,
29416
+ height
29417
+ } = backgroundImage ?? {};
29418
+ if (url && typeof url === "string") {
29419
+ const scale = box[0] / interactiveSizes$1.defaultBoxSize;
29420
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29421
+ style: {
29422
+ position: "absolute",
29423
+ bottom: 0,
29424
+ left: 0
29425
+ }
29426
+ }, /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref => {
29427
+ let {
29428
+ setAssetStatus
29429
+ } = _ref;
29430
+ return /*#__PURE__*/React__namespace.createElement(SvgImage, {
29431
+ src: url,
29432
+ width: width,
29433
+ height: height,
29434
+ scale: scale,
29435
+ responsive: false,
29436
+ setAssetStatus: setAssetStatus,
29437
+ alt: ""
29438
+ });
29439
+ }));
29440
+ }
29441
+ return null;
29442
+ };
29443
+
29444
+ /** Determine if coords is type CollinearTuple[] */
29445
+ const isCollinearTuples = coords => Array.isArray(coords[0][0]);
29446
+
29447
+ // Generic type makes returned state match input state
29448
+ function interactiveGraphReducer(state, action) {
29449
+ const {
29450
+ snapStep,
29451
+ range
29452
+ } = state;
29453
+ if (!state.coords) {
29454
+ throw new Error("Graph state must have been initialized with coords");
29455
+ }
29456
+ switch (action.type) {
29457
+ case MOVE_CONTROL_POINT:
29458
+ {
29459
+ const newCoords = action.itemIndex !== undefined && state.coords && isCollinearTuples(state.coords) ? updateAtIndex({
29460
+ array: state.coords,
29461
+ index: action.itemIndex,
29462
+ update: tuple => setAtIndex({
29463
+ array: tuple,
29464
+ index: action.pointIndex,
29465
+ newValue: snap({
29466
+ snapStep,
29467
+ point: bound$1({
29468
+ snapStep,
29469
+ range,
29470
+ point: action.destination
29471
+ })
29472
+ })
29473
+ })
29474
+ }) : setAtIndex({
29475
+ array: state.coords,
29476
+ index: action.pointIndex,
29477
+ newValue: snap({
29478
+ snapStep,
29479
+ point: bound$1({
29480
+ snapStep,
29481
+ range,
29482
+ point: action.destination
29483
+ })
29484
+ })
29485
+ });
29486
+
29487
+ // Cannot type narrow both conditions within function parameters,
29488
+ // so this may seem redundant, but it's necessary for type safety.
29489
+ const coordsToCheck = isCollinearTuples(newCoords) && action.itemIndex !== undefined ? newCoords[action.itemIndex] : newCoords;
29490
+ if (!isCollinearTuples(coordsToCheck) && coordsOverlap(coordsToCheck)) {
29491
+ return state;
29492
+ }
29493
+ return {
29494
+ ...state,
29495
+ hasBeenInteractedWith: true,
29496
+ coords: newCoords
29497
+ };
29498
+ }
29499
+ case MOVE_LINE:
29500
+ {
29501
+ if (!isCollinearTuples(state.coords)) {
29502
+ throw new Error("Cannot call this action unless coords is array of CollinearTuple.");
29503
+ }
29504
+ if (action.itemIndex === undefined) {
29505
+ throw new Error("Please provide index of line to move");
29506
+ }
29507
+ const currentLine = state.coords?.[action.itemIndex];
29508
+ if (!currentLine) {
29509
+ throw new Error("No line to move");
29510
+ }
29511
+ const change = getChange(currentLine, action.delta, {
29512
+ snapStep,
29513
+ range
29514
+ });
29515
+ const newLine = [snap({
29516
+ snapStep,
29517
+ point: mafs.vec.add(currentLine[0], change)
29518
+ }), snap({
29519
+ snapStep,
29520
+ point: mafs.vec.add(currentLine[1], change)
29521
+ })];
29522
+ const newCoords = setAtIndex({
29523
+ array: state.coords,
29524
+ index: action.itemIndex,
29525
+ newValue: newLine
29526
+ });
29527
+ return {
29528
+ ...state,
29529
+ hasBeenInteractedWith: true,
29530
+ coords: newCoords
29531
+ };
29532
+ }
29533
+ case MOVE_ALL:
29534
+ {
29535
+ if (isCollinearTuples(state.coords)) {
29536
+ throw new Error("Cannot call this action unless coords is array of vectors.");
29537
+ }
29538
+ const change = getChange(state.coords, action.delta, {
29539
+ snapStep,
29540
+ range
29541
+ });
29542
+ const newCoords = state.coords.map(point => snap({
29543
+ snapStep,
29544
+ point: mafs.vec.add(point, change)
29545
+ }));
29546
+ return {
29547
+ ...state,
29548
+ hasBeenInteractedWith: true,
29549
+ coords: newCoords
29550
+ };
29551
+ }
29552
+ default:
29553
+ throw new wonderStuffCore.UnreachableCaseError(action);
29554
+ }
29555
+ }
29556
+ const getChange = (coords, delta, constraintOpts) => {
29557
+ const [deltaX, deltaY] = delta;
29558
+ const maxMoves = coords.map(point => maxMove({
29559
+ ...constraintOpts,
29560
+ point
29561
+ }));
29562
+ const minMoves = coords.map(point => minMove({
29563
+ ...constraintOpts,
29564
+ point
29565
+ }));
29566
+ const maxXMove = Math.min(...maxMoves.map(move => move[0]));
29567
+ const maxYMove = Math.min(...maxMoves.map(move => move[1]));
29568
+ const minXMove = Math.max(...minMoves.map(move => move[0]));
29569
+ const minYMove = Math.max(...minMoves.map(move => move[1]));
29570
+ const dx = clamp(deltaX, minXMove, maxXMove);
29571
+ const dy = clamp(deltaY, minYMove, maxYMove);
29572
+ return [dx, dy];
29573
+ };
29574
+ function snap(_ref) {
29575
+ let {
29576
+ snapStep,
29577
+ point
29578
+ } = _ref;
29579
+ const [requestedX, requestedY] = point;
29580
+ const [snapX, snapY] = snapStep;
29581
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29582
+ }
29583
+
29584
+ // Returns the closest point to the given `point` that is within the graph
29585
+ // bounds given in `state`.
29586
+ function bound$1(_ref2) {
29587
+ let {
29588
+ snapStep,
29589
+ range,
29590
+ point
29591
+ } = _ref2;
29592
+ const [requestedX, requestedY] = point;
29593
+ const [snapX, snapY] = snapStep;
29594
+ const [[minX, maxX], [minY, maxY]] = range;
29595
+ return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
29596
+ }
29597
+
29598
+ // Returns the vector from the given point to the top-right corner of the graph
29599
+ function maxMove(_ref3) {
29600
+ let {
29601
+ snapStep,
29602
+ range,
29603
+ point
29604
+ } = _ref3;
29605
+ const topRight = bound$1({
29606
+ snapStep,
29607
+ range,
29608
+ point: [Infinity, Infinity]
29609
+ });
29610
+ return mafs.vec.sub(topRight, point);
29611
+ }
29612
+
29613
+ // Returns the vector from the given point to the bottom-left corner of the
29614
+ // graph
29615
+ function minMove(_ref4) {
29616
+ let {
29617
+ snapStep,
29618
+ range,
29619
+ point
29620
+ } = _ref4;
29621
+ const bottomLeft = bound$1({
29622
+ snapStep,
29623
+ range,
29624
+ point: [-Infinity, -Infinity]
29625
+ });
29626
+ return mafs.vec.sub(bottomLeft, point);
29627
+ }
29628
+ function clamp(value, min, max) {
29629
+ if (value < min) {
29630
+ return min;
29631
+ }
29632
+ if (value > max) {
29633
+ return max;
29634
+ }
29635
+ return value;
29636
+ }
29637
+ const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && kmath.vector.equal(coord, c)));
29638
+ function updateAtIndex(args) {
29639
+ const {
29640
+ array = [],
29641
+ index,
29642
+ update
29643
+ } = args;
29644
+ const newValue = update(array[index]);
29645
+ return setAtIndex({
29646
+ array,
29647
+ index,
29648
+ newValue
29649
+ });
29650
+ }
29651
+ function setAtIndex(args) {
29652
+ const {
29653
+ array = [],
29654
+ index,
29655
+ newValue
29656
+ } = args;
29657
+ const copy = [...array];
29658
+ copy[index] = newValue;
29659
+ // restoring readonly to array
29660
+ return copy;
29661
+ }
29662
+
29663
+ function initializeGraphState(params) {
29664
+ const {
29665
+ graph,
29666
+ step,
29667
+ snapStep,
29668
+ range
29669
+ } = params;
29670
+ switch (graph.type) {
29671
+ case "segment":
29672
+ return {
29673
+ type: "segment",
29674
+ hasBeenInteractedWith: false,
29675
+ range,
29676
+ snapStep,
29677
+ coords: getDefaultSegments({
29678
+ graph,
29679
+ step,
29680
+ range
29681
+ })
29682
+ };
29683
+ case "linear":
29684
+ case "linear-system":
29685
+ case "ray":
29686
+ return {
29687
+ type: graph.type,
29688
+ hasBeenInteractedWith: false,
29689
+ range,
29690
+ snapStep,
29691
+ // Linear and ray graphs have a single tuple of points, while a
29692
+ // linear system has two tuples of points.
29693
+ coords: getLineCoords({
29694
+ graph,
29695
+ range,
29696
+ step
29697
+ })
29698
+ };
29699
+ case "polygon":
29700
+ return {
29701
+ type: "polygon",
29702
+ hasBeenInteractedWith: false,
29703
+ range,
29704
+ snapStep,
29705
+ coords: getPolygonCoords({
29706
+ graph,
29707
+ range,
29708
+ step
29709
+ })
29710
+ };
29711
+ case "angle":
29712
+ case "point":
29713
+ case "circle":
29714
+ case "sinusoid":
29715
+ case "quadratic":
29716
+ throw new Error("Mafs not yet implemented for graph type: " + graph.type);
29717
+ default:
29718
+ throw new wonderStuffCore.UnreachableCaseError(graph);
29719
+ }
29720
+ }
29484
29721
 
29485
- /* A minor abstraction on top of NumberInput for ranges
29486
- *
29487
- */
29488
- class RangeInput extends React__namespace.Component {
29489
- static propTypes = {
29490
- value: PropTypes__default["default"].array.isRequired,
29491
- onChange: PropTypes__default["default"].func.isRequired,
29492
- placeholder: PropTypes__default["default"].array,
29493
- checkValidity: PropTypes__default["default"].func
29494
- };
29495
- static defaultProps = {
29496
- placeholder: [null, null]
29497
- };
29498
- render() {
29499
- const value = this.props.value;
29500
- const checkValidity = this.props.checkValidity || truth;
29501
- return /*#__PURE__*/React__namespace.createElement("div", {
29502
- className: "range-input"
29503
- }, /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29504
- value: value[0],
29505
- checkValidity: val => checkValidity([val, value[1]])
29506
- // eslint-disable-next-line react/jsx-no-bind
29507
- ,
29508
- onChange: this.onChange.bind(this, 0),
29509
- placeholder: this.props.placeholder[0]
29510
- })), /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29511
- value: value[1],
29512
- checkValidity: val => checkValidity([value[0], val])
29513
- // eslint-disable-next-line react/jsx-no-bind
29514
- ,
29515
- onChange: this.onChange.bind(this, 1),
29516
- placeholder: this.props.placeholder[1]
29517
- })));
29722
+ // TS v4 doesn't narrow return types, while v5 does.
29723
+ // Instead of updating to v5, using generic type to relate input and output types.
29724
+ function getGradableGraph(state, initialGraph) {
29725
+ if (!state.hasBeenInteractedWith) {
29726
+ return initialGraph;
29518
29727
  }
29519
- onChange = (i, newVal) => {
29520
- const value = this.props.value;
29521
- if (i === 0) {
29522
- this.props.onChange([newVal, value[1]]);
29523
- } else {
29524
- this.props.onChange([value[0], newVal]);
29728
+ switch (true) {
29729
+ // coords: Array of CollinearTuple
29730
+ case state.type === "linear-system" && initialGraph.type === "linear-system":
29731
+ case state.type === "segment" && initialGraph.type === "segment":
29732
+ return {
29733
+ ...initialGraph,
29734
+ coords: state.coords
29735
+ };
29736
+ // coords: CollinearTuple
29737
+ case state.type === "linear" && initialGraph.type === "linear":
29738
+ case state.type === "ray" && initialGraph.type === "ray":
29739
+ return {
29740
+ ...initialGraph,
29741
+ coords: state.coords?.[0]
29742
+ };
29743
+ case state.type === "polygon" && initialGraph.type === "polygon":
29744
+ return {
29745
+ ...initialGraph,
29746
+ coords: state.coords
29747
+ };
29748
+ default:
29749
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29750
+ }
29751
+ }
29752
+ const getDefaultSegments = _ref => {
29753
+ let {
29754
+ graph,
29755
+ range,
29756
+ step
29757
+ } = _ref;
29758
+ const ys = n => {
29759
+ switch (n) {
29760
+ case 2:
29761
+ return [5, -5];
29762
+ case 3:
29763
+ return [5, 0, -5];
29764
+ case 4:
29765
+ return [6, 2, -2, -6];
29766
+ case 5:
29767
+ return [6, 3, 0, -3, -6];
29768
+ case 6:
29769
+ return [5, 3, 1, -1, -3, -5];
29770
+ default:
29771
+ return [5];
29525
29772
  }
29526
29773
  };
29527
- }
29774
+ return ys(graph.numSegments).map(y => {
29775
+ let endpoints = [[-5, y], [5, y]];
29776
+ endpoints = normalizeCoords(endpoints, range);
29777
+ endpoints = normalizePoints(range, step, endpoints);
29778
+ return endpoints;
29779
+ });
29780
+ };
29781
+ const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
29782
+ const getLineCoords = _ref2 => {
29783
+ let {
29784
+ graph,
29785
+ range,
29786
+ step
29787
+ } = _ref2;
29788
+ return (
29789
+ // Return two lines for a linear system, one for a ray or linear
29790
+ graph.coords ?? graph.type === "linear-system" ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
29791
+ );
29792
+ };
29793
+ const getPolygonCoords = _ref3 => {
29794
+ let {
29795
+ graph,
29796
+ range,
29797
+ step
29798
+ } = _ref3;
29799
+ let coords = graph.coords;
29800
+ if (coords) {
29801
+ return coords;
29802
+ }
29803
+ const n = graph.numSides || 3;
29804
+ if (n === "unlimited") {
29805
+ coords = [];
29806
+ } else {
29807
+ const angle = 2 * Math.PI / n;
29808
+ const offset = (1 / n - 1 / 2) * Math.PI;
29528
29809
 
29529
- var components = /*#__PURE__*/Object.freeze({
29530
- __proto__: null,
29531
- ButtonGroup: ButtonGroup,
29532
- Graph: Graph,
29533
- Graphie: Graphie,
29534
- HUD: HUD,
29535
- Icon: Icon,
29536
- InfoTip: InfoTip,
29537
- InlineIcon: InlineIcon,
29538
- MathInput: MathInput,
29539
- MultiButtonGroup: MultiButtonGroup,
29540
- NumberInput: NumberInput,
29541
- PropCheckBox: PropCheckBox,
29542
- RangeInput: RangeInput,
29543
- SvgImage: SvgImage,
29544
- TextInput: TextInput,
29545
- TextListEditor: TextListEditor
29546
- });
29810
+ // TODO(alex): Generalize this to more than just triangles so that
29811
+ // all polygons have whole number side lengths if snapping to sides
29812
+ const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
29547
29813
 
29548
- /**
29549
- * If a graphie URL is provided in `backgroundImage`, will return the rendered graphie background.
29550
- * Otherwise, returns `null`.
29551
- */
29552
- const getLegacyGrid = (box, backgroundImage) => {
29553
- const {
29554
- url,
29555
- width,
29556
- height
29557
- } = backgroundImage ?? {};
29558
- if (url && typeof url === "string") {
29559
- const scale = box[0] / interactiveSizes$1.defaultBoxSize;
29560
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29561
- style: {
29562
- position: "absolute",
29563
- bottom: 0,
29564
- left: 0
29565
- }
29566
- }, /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref => {
29567
- let {
29568
- setAssetStatus
29569
- } = _ref;
29570
- return /*#__PURE__*/React__namespace.createElement(SvgImage, {
29571
- src: url,
29572
- width: width,
29573
- height: height,
29574
- scale: scale,
29575
- responsive: false,
29576
- setAssetStatus: setAssetStatus,
29577
- alt: ""
29578
- });
29579
- }));
29814
+ // Generate coords of a regular polygon with n sides
29815
+ coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
29580
29816
  }
29581
- return null;
29817
+ coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29818
+ const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
29819
+ coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
29820
+ return coords;
29582
29821
  };
29583
29822
 
29584
29823
  const renderGraph = props => {
@@ -29606,6 +29845,11 @@ const renderGraph = props => {
29606
29845
  graphState: state,
29607
29846
  dispatch: dispatch
29608
29847
  });
29848
+ case "polygon":
29849
+ return /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
29850
+ graphState: state,
29851
+ dispatch: dispatch
29852
+ });
29609
29853
  default:
29610
29854
  return new wonderStuffCore.UnreachableCaseError(type);
29611
29855
  }
@@ -29619,8 +29863,8 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
29619
29863
  }));
29620
29864
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29621
29865
  style: {
29622
- width: props.backgroundImage?.width ?? width,
29623
- height: props.backgroundImage?.height ?? height,
29866
+ width,
29867
+ height,
29624
29868
  position: "relative"
29625
29869
  }
29626
29870
  }, legacyGrid, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
@@ -29703,7 +29947,7 @@ const makeInvalidTypeError = (functionName, graphType) => {
29703
29947
 
29704
29948
  // There's no transform function in exports
29705
29949
 
29706
- class InteractiveGraph extends React__namespace.Component {
29950
+ class LegacyInteractiveGraph extends React__namespace.Component {
29707
29951
  // @ts-expect-error - TS2564 - Property 'points' has no initializer and is not definitely assigned in the constructor.
29708
29952
 
29709
29953
  // @ts-expect-error - TS2564 - Property 'shouldResetGraphie' has no initializer and is not definitely assigned in the constructor.
@@ -29723,7 +29967,6 @@ class InteractiveGraph extends React__namespace.Component {
29723
29967
  type: "linear"
29724
29968
  }
29725
29969
  };
29726
- mafsRef = /*#__PURE__*/React__namespace.createRef();
29727
29970
  state = {
29728
29971
  shouldShowInstructions: _getShouldShowInstructions(this.props)
29729
29972
  };
@@ -30834,26 +31077,13 @@ class InteractiveGraph extends React__namespace.Component {
30834
31077
  graph: graph
30835
31078
  });
30836
31079
  };
30837
- getUserInput = () => this.mafsRef.current?.getUserInput?.() ?? InteractiveGraph.getUserInputFromProps(this.props);
31080
+ getUserInput = () => InteractiveGraph.getUserInputFromProps(this.props);
30838
31081
  simpleValidate = rubric => InteractiveGraph.validate(this.getUserInput(), rubric, this);
30839
31082
  focus = $__default["default"].noop;
30840
-
30841
- // const segment: boolean = useFeatureIsOn("mafs-segment-graph");
30842
- // <Renderer apiOptions={{flags: {mafs: {segment}}}}
30843
31083
  render() {
30844
31084
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
30845
31085
  const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
30846
31086
  const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
30847
-
30848
- // Mafs shim
30849
- if (this.props.apiOptions?.flags?.["mafs"]?.[this.props.graph.type]) {
30850
- return /*#__PURE__*/React__namespace.createElement(MafsGraph, _extends({}, this.props, {
30851
- ref: this.mafsRef,
30852
- gridStep: gridStep,
30853
- snapStep: snapStep,
30854
- box: box
30855
- }));
30856
- }
30857
31087
  let instructions;
30858
31088
  // isClickToAddPoints() only applies to points and polygons
30859
31089
  if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
@@ -30899,6 +31129,52 @@ class InteractiveGraph extends React__namespace.Component {
30899
31129
  isMobile: isMobile
30900
31130
  }));
30901
31131
  }
31132
+ }
31133
+ class InteractiveGraph extends React__namespace.Component {
31134
+ legacyGraphRef = /*#__PURE__*/React__namespace.createRef();
31135
+ mafsRef = /*#__PURE__*/React__namespace.createRef();
31136
+ static defaultProps = {
31137
+ labels: ["x", "y"],
31138
+ range: [[-10, 10], [-10, 10]],
31139
+ step: [1, 1],
31140
+ backgroundImage: defaultBackgroundImage,
31141
+ markings: "graph",
31142
+ showTooltips: false,
31143
+ showProtractor: false,
31144
+ showRuler: false,
31145
+ rulerLabel: "",
31146
+ rulerTicks: 10,
31147
+ graph: {
31148
+ type: "linear"
31149
+ }
31150
+ };
31151
+ getUserInput = () => {
31152
+ if (this.mafsRef.current) {
31153
+ return this.mafsRef.current.getUserInput?.();
31154
+ }
31155
+ if (this.legacyGraphRef.current) {
31156
+ return this.legacyGraphRef.current.getUserInput();
31157
+ }
31158
+ throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
31159
+ };
31160
+ simpleValidate = rubric => InteractiveGraph.validate(this.getUserInput?.(), rubric, this);
31161
+ render() {
31162
+ // Mafs shim
31163
+ if (this.props.apiOptions?.flags?.["mafs"]?.[this.props.graph.type]) {
31164
+ const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
31165
+ const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
31166
+ const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
31167
+ return /*#__PURE__*/React__namespace.createElement(MafsGraph, _extends({}, this.props, {
31168
+ ref: this.mafsRef,
31169
+ gridStep: gridStep,
31170
+ snapStep: snapStep,
31171
+ box: box
31172
+ }));
31173
+ }
31174
+ return /*#__PURE__*/React__namespace.createElement(LegacyInteractiveGraph, _extends({
31175
+ ref: this.legacyGraphRef
31176
+ }, this.props));
31177
+ }
30902
31178
  static getQuadraticCoefficients(coords) {
30903
31179
  const p1 = coords[0];
30904
31180
  const p2 = coords[1];
@@ -41618,7 +41894,7 @@ const init = function (options) {
41618
41894
 
41619
41895
  // This file is processed by a Rollup plugin (replace) to inject the production
41620
41896
  const libName = "@khanacademy/perseus";
41621
- const libVersion = "20.5.0";
41897
+ const libVersion = "20.7.0";
41622
41898
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
41623
41899
 
41624
41900
  const apiVersion = {
@@ -42390,7 +42666,6 @@ class ServerItemRenderer extends React__namespace.Component {
42390
42666
  const qScore = this.questionRenderer.scoreWidgets();
42391
42667
  const qGuess = this.questionRenderer.getUserInputForWidgets();
42392
42668
  const state = this.questionRenderer.getSerializedState();
42393
- // @ts-expect-error - TS2322 - Type 'Partial<Record<string, KEScore>>' is not assignable to type '{ [key: string]: KEScore; }'. | TS2345 - Argument of type '{ [widgetId: string]: PerseusScore; }' is not assignable to parameter of type 'Partial<Record<string, { type: "invalid"; message?: string | null | undefined; suppressAlmostThere?: boolean | null | undefined; }>>'.
42394
42669
  return mapObject(qScore, (score, id) => {
42395
42670
  return Util.keScoreFromPerseusScore(score, qGuess[id], state);
42396
42671
  });
@@ -44322,6 +44597,218 @@ function generateTestPerseusItem() {
44322
44597
  };
44323
44598
  }
44324
44599
 
44600
+ /**
44601
+ * Some helpers for CEDAR logging. CEDAR convention dictates
44602
+ * that enum values should be upper case. Ex: for the
44603
+ * PerseusWidgetUsedInProblemAttempt CEDAR event, we needed
44604
+ * to: get the type and convert that to an enum value.
44605
+ */
44606
+
44607
+ const widgetNameToEnum = {
44608
+ categorizer: "CATEGORIZER",
44609
+ "cs-program": "CS_PROGRAM",
44610
+ definition: "DEFINITION",
44611
+ "deprecated-standin": "DEPRECATED_STANDIN",
44612
+ dropdown: "DROPDOWN",
44613
+ explanation: "EXPLANATION",
44614
+ expression: "EXPRESSION",
44615
+ "graded-group-set": "GRADED_GROUP_SET",
44616
+ "graded-group": "GRADED_GROUP",
44617
+ grapher: "GRAPHER",
44618
+ group: "GROUP",
44619
+ iframe: "IFRAME",
44620
+ image: "IMAGE",
44621
+ "input-number": "INPUT_NUMBER",
44622
+ interaction: "INTERACTIVE",
44623
+ "interactive-graph": "INTERACTIVE_GRAPH",
44624
+ "label-image": "LABEL_IMAGE",
44625
+ "lights-puzzle": "LIGHTS_PUZZLE",
44626
+ matcher: "MATCHER",
44627
+ matrix: "MATRIX",
44628
+ measurer: "MEASURER",
44629
+ "molecule-renderer": "MOLECULE",
44630
+ "number-line": "NUMBER_LINE",
44631
+ "numeric-input": "NUMERIC_INPUT",
44632
+ orderer: "ORDERER",
44633
+ passage: "PASSAGE",
44634
+ "passage-ref": "PASSAGE_REF",
44635
+ "passage-ref-target": "PASSAGE_REF_TARGET",
44636
+ plotter: "PLOTTER",
44637
+ "python-program": "PYTHON_PROGRAM",
44638
+ "reaction-diagram": "REACTION_DIAGRAM",
44639
+ radio: "RADIO",
44640
+ sequence: "SEQUENCE",
44641
+ simulator: "SIMULATOR",
44642
+ sorter: "SORTER",
44643
+ table: "TABLE",
44644
+ "unit-input": "UNIT",
44645
+ video: "VIDEO"
44646
+ };
44647
+ function convertWidgetNameToEnum(name) {
44648
+ const widgetEnum = widgetNameToEnum[name];
44649
+ if (!widgetEnum) {
44650
+ throw new Error(`Unknown widget name: ${name}`);
44651
+ }
44652
+ return widgetEnum;
44653
+ }
44654
+ function getWidgetTypeFromWidgetId(widgetId) {
44655
+ const [widgetType, widgetNumber] = widgetId.split(" ");
44656
+ if (!widgetNumber) {
44657
+ throw new Error(`Incorrect format for widget ID: ${widgetId}`);
44658
+ }
44659
+ if (!widgetNameToEnum[widgetType]) {
44660
+ throw new Error(`Unknown widget type (${widgetType}) in widget ID: ${widgetId}`);
44661
+ }
44662
+ return widgetType;
44663
+ }
44664
+
44665
+ let WidgetType = /*#__PURE__*/function (WidgetType) {
44666
+ WidgetType["Categorizer"] = "categorizer";
44667
+ WidgetType["CSProgram"] = "cs-program";
44668
+ WidgetType["Definition"] = "definition";
44669
+ WidgetType["Dropdown"] = "dropdown";
44670
+ WidgetType["Explanation"] = "explanation";
44671
+ WidgetType["Expression"] = "expression";
44672
+ WidgetType["GradedGroup"] = "graded-group";
44673
+ WidgetType["GradedGroupAnswerBar"] = "graded-group-answer-bar";
44674
+ WidgetType["GradedGroupSet"] = "graded-group-set";
44675
+ WidgetType["Grapher"] = "grapher";
44676
+ WidgetType["Group"] = "group";
44677
+ WidgetType["Iframe"] = "iframe";
44678
+ WidgetType["Image"] = "image";
44679
+ WidgetType["InputNumber"] = "input-number";
44680
+ WidgetType["Interaction"] = "interaction";
44681
+ WidgetType["InteractiveGraph"] = "interactive-graph";
44682
+ WidgetType["InteractiveNumberLine"] = "interactive-number-line";
44683
+ WidgetType["LabelImage"] = "label-image";
44684
+ WidgetType["Matcher"] = "matcher";
44685
+ WidgetType["Matrix"] = "matrix";
44686
+ WidgetType["Measurer"] = "measurer";
44687
+ WidgetType["Molecule"] = "molecule";
44688
+ WidgetType["NumberLine"] = "number-line";
44689
+ WidgetType["NumericInput"] = "numeric-input";
44690
+ WidgetType["Orderer"] = "orderer";
44691
+ WidgetType["Passage"] = "passage";
44692
+ WidgetType["PassageRef"] = "passage-ref";
44693
+ WidgetType["PassageRefTarget"] = "passage-ref-target";
44694
+ WidgetType["Plotter"] = "plotter";
44695
+ WidgetType["Protractor"] = "protractor";
44696
+ WidgetType["PythonProgram"] = "python-program";
44697
+ WidgetType["Radio"] = "radio";
44698
+ WidgetType["Sorter"] = "sorter";
44699
+ WidgetType["Table"] = "table";
44700
+ WidgetType["Unit"] = "unit";
44701
+ WidgetType["Video"] = "video";
44702
+ WidgetType["VideoTranscriptLink"] = "video-transcript-link";
44703
+ return WidgetType;
44704
+ }({});
44705
+ // These are all the current widgets that require user-input and are supported for automatic scoring
44706
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44707
+
44708
+ /**
44709
+ * Regex for widget placeholders in a string.
44710
+ *
44711
+ * First capture group is the widget ID (ex. 'radio 1')
44712
+ * Second capture group is the widget type (ex. "radio)
44713
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44714
+ */
44715
+ function getWidgetRegex() {
44716
+ return /\[\[☃ (([a-z-]+) \d+)\]\]/g;
44717
+ }
44718
+
44719
+ /**
44720
+ * Add a widget placeholder using the provided widget type and instance number.
44721
+ * ex. addWidget(WidgetType.Radio, 1) => "[[☃ radio 1]]"
44722
+ *
44723
+ * @param {WidgetType} widgetType
44724
+ * @param {number} instance
44725
+ * @returns {string}
44726
+ */
44727
+ function addWidget(widgetType, instance) {
44728
+ return `[[☃ ${widgetType} ${String(instance)}]]`;
44729
+ }
44730
+
44731
+ /**
44732
+ * Extract all widget IDs, which includes the widget type and instance number.
44733
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44734
+ *
44735
+ * Content should contain Perseus widget placeholders,
44736
+ * which look like: '[[☃ radio 1]]'.
44737
+ *
44738
+ * @param {string} content
44739
+ * @returns {Array<WidgetId>} widgets
44740
+ */
44741
+ function getAllWidgetIds(content) {
44742
+ const widgets = [];
44743
+ const localWidgetRegex = getWidgetRegex();
44744
+ let match = localWidgetRegex.exec(content);
44745
+ while (match !== null) {
44746
+ widgets.push(match[1]);
44747
+ match = localWidgetRegex.exec(content);
44748
+ }
44749
+ return widgets;
44750
+ }
44751
+
44752
+ /**
44753
+ * Check if a specific widget is an actual widget type using its name.
44754
+ * The widget name should include a widget type, without an instance number,
44755
+ * as a string. ex. 'radio'
44756
+ *
44757
+ * @param {string} widgetName
44758
+ * @returns {boolean}
44759
+ */
44760
+ function isWidgetType(widgetName) {
44761
+ return Object.values(WidgetType).includes(widgetName);
44762
+ }
44763
+
44764
+ /**
44765
+ * Extract all widget types from a Perseus JSON content string.
44766
+ * This does not include the instance number and prevents duplicates.
44767
+ * example output: ['radio', 'categorizer']
44768
+ *
44769
+ * @param {string} content
44770
+ * @returns {Array<WidgetId>}
44771
+ */
44772
+ function getAllWidgetTypes(content) {
44773
+ const widgetTypes = [];
44774
+ const localWidgetRegex = getWidgetRegex();
44775
+ let match = localWidgetRegex.exec(content);
44776
+ while (match !== null) {
44777
+ const matchType = match[2];
44778
+ if (!widgetTypes.includes(matchType) && isWidgetType(matchType)) {
44779
+ widgetTypes.push(matchType);
44780
+ }
44781
+ match = localWidgetRegex.exec(content);
44782
+ }
44783
+ return widgetTypes;
44784
+ }
44785
+
44786
+ /**
44787
+ * Check if a specific widget is a question widget type using its ID.
44788
+ * The widget ID includes the widget type and the instance number.
44789
+ *
44790
+ * @param {WidgetId} widgetId
44791
+ * @returns {boolean}
44792
+ */
44793
+ function isQuestionWidgetType(widgetId) {
44794
+ return QUESTION_WIDGETS.includes(widgetId.split(" ")[0]);
44795
+ }
44796
+
44797
+ /**
44798
+ * Extract the widget IDs of all widgets considered question widgets. Widget IDs
44799
+ * include widget type and instance number.
44800
+ * example output: ['radio 1', 'categorizer 1']
44801
+ *
44802
+ * Content should contain Perseus widget placeholders,
44803
+ * which look like: '[[☃ radio 1]]'.
44804
+ *
44805
+ * @param {string} content
44806
+ * @returns Array<WidgetId>
44807
+ */
44808
+ function getQuestionWidgetIds(content) {
44809
+ return getAllWidgetIds(content).filter(isQuestionWidgetType);
44810
+ }
44811
+
44325
44812
  const EditorJsonify = {
44326
44813
  serialize: function () {
44327
44814
  // Omit props that get passed to all widgets
@@ -44363,6 +44850,7 @@ exports.PerseusError = PerseusError;
44363
44850
  exports.PerseusExpressionAnswerFormConsidered = PerseusExpressionAnswerFormConsidered;
44364
44851
  exports.PerseusMarkdown = PerseusMarkdown;
44365
44852
  exports.PlotterWidget = Plotter$1;
44853
+ exports.QUESTION_WIDGETS = QUESTION_WIDGETS;
44366
44854
  exports.Radio = Radio;
44367
44855
  exports.Renderer = Renderer;
44368
44856
  exports.ServerItemRenderer = ref;
@@ -44371,16 +44859,23 @@ exports.TableWidget = Table$1;
44371
44859
  exports.Util = Util;
44372
44860
  exports.WIDGET_PROP_DENYLIST = denylist;
44373
44861
  exports.WidgetJsonifyDeprecated = WidgetJsonifyDeprecated;
44862
+ exports.WidgetType = WidgetType;
44374
44863
  exports.Widgets = widgets$1;
44864
+ exports.addWidget = addWidget;
44375
44865
  exports.apiVersion = apiVersion;
44376
44866
  exports.buildEmptyItemTreeForShape = buildEmptyItemTreeForShape;
44377
44867
  exports.buildMapper = buildMapper;
44378
44868
  exports.components = components;
44379
44869
  exports.constants = constants;
44870
+ exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
44380
44871
  exports.displaySigFigs = displaySigFigs;
44381
44872
  exports.generateTestPerseusItem = generateTestPerseusItem;
44382
44873
  exports.genericPerseusItemData = genericPerseusItemData;
44874
+ exports.getAllWidgetIds = getAllWidgetIds;
44875
+ exports.getAllWidgetTypes = getAllWidgetTypes;
44383
44876
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
44877
+ exports.getQuestionWidgetIds = getQuestionWidgetIds;
44878
+ exports.getWidgetTypeFromWidgetId = getWidgetTypeFromWidgetId;
44384
44879
  exports.globalConstants = globalConstants;
44385
44880
  exports.globalStyles = globalStyles;
44386
44881
  exports.icons = iconPaths;