@khanacademy/perseus 20.8.1 → 21.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/es/index.js +716 -359
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/index.d.ts +5 -4
  4. package/dist/index.js +739 -366
  5. package/dist/index.js.map +1 -1
  6. package/dist/types.d.ts +2 -0
  7. package/dist/util/extract-perseus-data.d.ts +7 -8
  8. package/dist/util/snowman-utils.d.ts +0 -72
  9. package/dist/util/test-utils.d.ts +37 -2
  10. package/dist/util/widget-enum-utils.d.ts +1 -3
  11. package/dist/widget-type-utils.d.ts +64 -0
  12. package/dist/widgets/interactive-graph.d.ts +3 -3
  13. package/dist/widgets/interactive-graphs/axis-arrows.d.ts +2 -0
  14. package/dist/widgets/interactive-graphs/graphs/components/angle.d.ts +12 -2
  15. package/dist/widgets/interactive-graphs/graphs/components/css-transform-wrapper.d.ts +7 -0
  16. package/dist/widgets/interactive-graphs/graphs/components/text-label.d.ts +6 -0
  17. package/dist/widgets/interactive-graphs/graphs/point.d.ts +5 -0
  18. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +2 -0
  19. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +15 -74
  20. package/dist/widgets/interactive-graphs/mafs-supported-graph-types.d.ts +1 -1
  21. package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +10 -3
  22. package/dist/widgets/interactive-graphs/reducer/interactive-graph-reducer.d.ts +1 -1
  23. package/dist/widgets/interactive-graphs/reducer/interactive-graph-state.d.ts +1 -1
  24. package/dist/widgets/interactive-graphs/reducer/use-graph-state.d.ts +11 -0
  25. package/dist/widgets/interactive-graphs/types.d.ts +30 -9
  26. package/dist/widgets/interactive-graphs/utils.d.ts +2 -2
  27. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18890,7 +18890,7 @@ const isAccessible = widgetInfo => {
18890
18890
  }
18891
18891
  return !!accessible;
18892
18892
  };
18893
- const getAllWidgetTypes$1 = () => {
18893
+ const getAllWidgetTypes = () => {
18894
18894
  return ___default["default"].keys(widgets);
18895
18895
  };
18896
18896
  const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
@@ -19162,7 +19162,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19162
19162
  getVersionVector: getVersionVector,
19163
19163
  getPublicWidgets: getPublicWidgets,
19164
19164
  isAccessible: isAccessible,
19165
- getAllWidgetTypes: getAllWidgetTypes$1,
19165
+ getAllWidgetTypes: getAllWidgetTypes,
19166
19166
  upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
19167
19167
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
19168
19168
  traverseChildWidgets: traverseChildWidgets$3,
@@ -19560,6 +19560,9 @@ class Renderer extends React__namespace.Component {
19560
19560
  if (!widgetInfo.type || !widgetInfo.alignment) {
19561
19561
  const newValues = {};
19562
19562
  if (!widgetInfo.type) {
19563
+ // TODO: why does widget have no type?
19564
+ // We don't want to derive type from widget ID
19565
+ // see: LEMS-1845
19563
19566
  newValues.type = widgetId.split(" ")[0];
19564
19567
  }
19565
19568
  if (!widgetInfo.alignment) {
@@ -25023,9 +25026,22 @@ class Explanation extends React__namespace.Component {
25023
25026
  // Special styling is needed to fit the button in a block of text without throwing off the line spacing.
25024
25027
  // While the button is not normally included in a block of text, it needs to be able to accommodate such a case.
25025
25028
  const buttonStyleOverrides = {
25026
- height: "22px",
25029
+ height: "auto",
25027
25030
  marginLeft: "-2px",
25028
- padding: "0 2px"
25031
+ // transfer space on the left side to the padding
25032
+ marginRight: "2px",
25033
+ // adds space between the right border and any text that follows
25034
+ paddingLeft: "2px" // adds space between the left border and the button text
25035
+ };
25036
+
25037
+ const labelStyle = {
25038
+ fontSize: "18px",
25039
+ lineHeight: "20px",
25040
+ // The following property adjusts the large space between the button text and the caret icon.
25041
+ // Since we are unable to adjust the styling of the icon (where the extra space exists),
25042
+ // we are adjusting it on the text label by using a negative margin.
25043
+ marginRight: "-6px",
25044
+ "white-space": "normal"
25029
25045
  };
25030
25046
  const contentStyling = [styles$p.content, this.state.expanded ? styles$p.contentExpanded : styles$p.contentCollapsed, allowTransition && (this.state.expanded ? styles$p.transitionExpanded : styles$p.transitionCollapsed)];
25031
25047
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
@@ -25036,8 +25052,9 @@ class Explanation extends React__namespace.Component {
25036
25052
  "aria-controls": ids.get("content"),
25037
25053
  endIcon: caretIcon,
25038
25054
  kind: "tertiary",
25055
+ labelStyle: labelStyle,
25039
25056
  onClick: this._onClick,
25040
- size: "large",
25057
+ size: "small",
25041
25058
  style: buttonStyleOverrides
25042
25059
  }, promptText), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
25043
25060
  id: ids.get("content"),
@@ -28339,6 +28356,14 @@ const moveAll = delta => ({
28339
28356
  type: MOVE_ALL,
28340
28357
  delta
28341
28358
  });
28359
+ const MOVE_POINT = "move-point";
28360
+ function movePoint(index, destination) {
28361
+ return {
28362
+ type: MOVE_POINT,
28363
+ index,
28364
+ destination
28365
+ };
28366
+ }
28342
28367
 
28343
28368
  /**
28344
28369
  * 44 is touch best practice and AAA compliant for WCAG
@@ -28416,6 +28441,10 @@ const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
28416
28441
  return [xMax, yAtXMax];
28417
28442
  }
28418
28443
  };
28444
+ const getLines = points => points.map((point, i) => {
28445
+ const next = points[(i + 1) % points.length];
28446
+ return [point, next];
28447
+ });
28419
28448
 
28420
28449
  const defaultStroke = "var(--movable-line-stroke-color)";
28421
28450
  const MovableLine = props => {
@@ -28560,7 +28589,9 @@ const SegmentGraph = props => {
28560
28589
  onMoveLine: delta => {
28561
28590
  dispatch(moveLine(i, delta));
28562
28591
  },
28563
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28592
+ onMovePoint: (endpointIndex, destination) => {
28593
+ dispatch(moveControlPoint(endpointIndex, destination, i));
28594
+ },
28564
28595
  "data-testid": "segment" + i
28565
28596
  })));
28566
28597
  };
@@ -28676,12 +28707,55 @@ const RayGraph = props => {
28676
28707
  }));
28677
28708
  };
28678
28709
 
28710
+ /**
28711
+ * Wrapper with Mafs transform styles for SVG elements.
28712
+ * Scales and translates the SVG element to match the Mafs view.
28713
+ * Use if you do not need/want to use the hook in `use-transform.ts`.
28714
+ */
28715
+ const MafsCssTransformWrapper = _ref => {
28716
+ let {
28717
+ children
28718
+ } = _ref;
28719
+ return /*#__PURE__*/React__namespace.createElement("g", {
28720
+ style: {
28721
+ transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28722
+ }
28723
+ }, children);
28724
+ };
28725
+
28726
+ const TextLabel = _ref => {
28727
+ let {
28728
+ children,
28729
+ ...rest
28730
+ } = _ref;
28731
+ return /*#__PURE__*/React__default["default"].createElement(mafs.Text, _extends({
28732
+ size: 16,
28733
+ svgTextProps: {
28734
+ filter: "url(#background)",
28735
+ fontWeight: "bold"
28736
+ }
28737
+ }, rest), children);
28738
+ };
28739
+ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs", null, /*#__PURE__*/React__default["default"].createElement("filter", {
28740
+ id: "background",
28741
+ x: "-5%",
28742
+ width: "110%",
28743
+ y: "0%",
28744
+ height: "100%"
28745
+ }, /*#__PURE__*/React__default["default"].createElement("feFlood", {
28746
+ floodColor: "#FFF",
28747
+ floodOpacity: "0.64"
28748
+ }), /*#__PURE__*/React__default["default"].createElement("feComposite", {
28749
+ operator: "over",
28750
+ in: "SourceGraphic"
28751
+ })));
28752
+
28679
28753
  const Angle = _ref => {
28680
28754
  let {
28681
28755
  centerPoint,
28682
28756
  endPoints,
28683
28757
  range,
28684
- polygonPoints,
28758
+ polygonLines,
28685
28759
  showAngles
28686
28760
  } = _ref;
28687
28761
  const [centerX, centerY] = centerPoint;
@@ -28703,7 +28777,7 @@ const Angle = _ref => {
28703
28777
  // Used to calculate the inside of the angle and to create square
28704
28778
  const [x3, y3] = mafs.vec.add(centerPoint, mafs.vec.add(mafs.vec.sub([x1, y1], centerPoint), mafs.vec.sub([x2, y2], centerPoint)));
28705
28779
  if (!showAngles) {
28706
- return isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleArc, {
28780
+ return isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
28707
28781
  start: [x1, y1],
28708
28782
  vertex: [x2, y2],
28709
28783
  end: [x3, y3]
@@ -28711,13 +28785,13 @@ const Angle = _ref => {
28711
28785
  }
28712
28786
 
28713
28787
  // Midpoint betwen ends of arc
28714
- const isInside = shouldDrawArcInside([x3, y3], centerPoint, range, polygonPoints);
28715
- const largeArcFlag = isInside ? 1 : 0;
28716
- const sweepFlag = isInside ? 1 : 0;
28788
+ const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
28789
+ const largeArcFlag = isOutside ? 1 : 0;
28790
+ const sweepFlag = isOutside ? 1 : 0;
28717
28791
  const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
28718
28792
  let angleInDegrees = angle * (180 / Math.PI);
28719
28793
  // If we have triggered "largArcFlag", the angle should be greater than 180
28720
- if (isInside) {
28794
+ if (isOutside) {
28721
28795
  angleInDegrees = 360 - angleInDegrees;
28722
28796
  }
28723
28797
  const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
@@ -28734,92 +28808,63 @@ const Angle = _ref => {
28734
28808
  }), /*#__PURE__*/React__namespace.createElement("feComposite", {
28735
28809
  operator: "over",
28736
28810
  in: "SourceGraphic"
28737
- }))), !isInside && isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleArc, {
28811
+ }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
28738
28812
  start: [x1, y1],
28739
28813
  vertex: [x2, y2],
28740
28814
  end: [x3, y3]
28741
- }) : /*#__PURE__*/React__namespace.createElement("g", {
28742
- style: {
28743
- transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28744
- }
28745
- }, /*#__PURE__*/React__namespace.createElement("path", {
28815
+ }) : /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
28746
28816
  d: arc,
28747
28817
  strokeWidth: 0.02,
28748
28818
  fill: "none"
28749
- })), /*#__PURE__*/React__namespace.createElement(mafs.Text, {
28819
+ })), /*#__PURE__*/React__namespace.createElement(TextLabel, {
28750
28820
  x: x3,
28751
28821
  y: y3,
28752
- size: 15,
28753
- svgTextProps: {
28754
- filter: "url(#background)",
28755
- fontWeight: "bold"
28756
- }
28757
- // Shift position if text is too close to movable point
28758
- ,
28759
28822
  attach: y3 - centerY > 0 ? "s" : "n",
28760
28823
  attachDistance: Math.abs(y3 - centerY) < 0.2 || mafs.vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
28761
28824
  }, angleLabel, "\xB0"));
28762
28825
  };
28763
- const RightAngleArc = _ref2 => {
28826
+
28827
+ /**
28828
+ * This is broken out into its own component so that it can be used for an early return
28829
+ * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
28830
+ */
28831
+ const RightAngleSquare = _ref2 => {
28764
28832
  let {
28765
28833
  start: [x1, y1],
28766
28834
  vertex: [x2, y2],
28767
28835
  end: [x3, y3]
28768
28836
  } = _ref2;
28769
- return /*#__PURE__*/React__namespace.createElement("g", {
28770
- style: {
28771
- transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28772
- }
28773
- }, /*#__PURE__*/React__namespace.createElement("path", {
28837
+ return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
28774
28838
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
28775
28839
  strokeWidth: 0.02,
28776
28840
  fill: "none"
28777
28841
  }));
28778
28842
  };
28779
28843
  const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
28780
- const shouldDrawArcInside = (midPoint, vertex, range, polygonPoints) => {
28781
- const rangeIntersectionPoint = getRayIntersectionCoords(vertex, midPoint, range);
28782
- const lines = getLines(polygonPoints);
28783
- let lineIntersections = 0;
28784
- lines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersections++);
28785
-
28786
- // The intersection check will sometimes return false if it intersects with
28787
- // another vertex, so in the case we get 0 intersections, we check for points.
28788
- if (lineIntersections === 0) {
28789
- // find point in array
28790
- const midpointIndex = polygonPoints.findIndex(_ref3 => {
28791
- let [x, y] = _ref3;
28792
- return x === midPoint[0] && y === midPoint[1];
28793
- });
28794
- // get array sans point and adjacent points
28795
- const [prev, next] = [(midpointIndex - 1 + polygonPoints.length) % polygonPoints.length, (midpointIndex + 1) % polygonPoints.length];
28796
- const [[lineAx, lineAy], [lineBx, lineBy]] = [midPoint, rangeIntersectionPoint];
28797
- for (let i = 0; i < polygonPoints.length; i++) {
28798
- if ([prev, midpointIndex, next].includes(i)) {
28799
- continue;
28800
- }
28801
- const [x, y] = polygonPoints[i];
28802
- const intersectsPoint = (y - lineAy) * (lineBx - lineAx) === (lineBy - lineAy) * (x - lineAx);
28803
- if (intersectsPoint) {
28804
- return false;
28805
- }
28806
- }
28807
- return true;
28808
- }
28844
+
28845
+ /**
28846
+ * Determines if an angle is an inside (false) or outside (true) angle.
28847
+ * They way, we know to flip the `largeArc` and `sweepArc` flags.
28848
+ * Uses the priciple that a ray from a point inside a polygon will intersect
28849
+ * with an odd number of lines, while a ray from a point outside the polygon
28850
+ * will intersect with an even number of lines.
28851
+ * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
28852
+ */
28853
+ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
28854
+ // Create a ray from the midpoint (inside angle) to the edge of the range
28855
+ const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
28856
+ let lineIntersectionCount = 0;
28857
+ polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
28809
28858
 
28810
28859
  // If the number of intersections is even, the angle is inside the polygon
28811
- return isEven(lineIntersections);
28860
+ return !isEven(lineIntersectionCount);
28812
28861
  };
28813
- const getLines = points => points.map((point, i) => {
28814
- const next = points[(i + 1) % points.length];
28815
- return [point, next];
28816
- });
28817
28862
 
28818
28863
  // https://stackoverflow.com/a/24392281/7347484
28819
28864
  // The "intersects" function in geometry doesn't seem to work for this use case
28820
- const linesIntersect = (_ref4, _ref5) => {
28821
- let [[a, b], [c, d]] = _ref4;
28822
- let [[p, q], [r, s]] = _ref5;
28865
+ const linesIntersect = (_ref3, _ref4) => {
28866
+ let [[a, b], [c, d]] = _ref3;
28867
+ let [[p, q], [r, s]] = _ref4;
28823
28868
  const determinant = (c - a) * (s - q) - (r - p) * (d - b);
28824
28869
  if (determinant === 0) {
28825
28870
  return false;
@@ -28841,6 +28886,7 @@ const PolygonGraph = props => {
28841
28886
  coords,
28842
28887
  type,
28843
28888
  showAngles,
28889
+ showSides,
28844
28890
  range
28845
28891
  } = props.graphState;
28846
28892
  const points = coords ?? [[0, 0]];
@@ -28859,6 +28905,7 @@ const PolygonGraph = props => {
28859
28905
  constrain: p => p
28860
28906
  });
28861
28907
  const active = hovered || focused || dragging;
28908
+ const lines = getLines(points);
28862
28909
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28863
28910
  points: [...points],
28864
28911
  color: "var(--movable-line-stroke-color)",
@@ -28877,9 +28924,18 @@ const PolygonGraph = props => {
28877
28924
  endPoints: [pt1, pt2],
28878
28925
  active: active,
28879
28926
  range: range,
28880
- polygonPoints: points,
28927
+ polygonLines: lines,
28881
28928
  showAngles: !!showAngles
28882
28929
  });
28930
+ }), showSides && lines.map((_ref, i) => {
28931
+ let [start, end] = _ref;
28932
+ const [x, y] = mafs.vec.midpoint(start, end);
28933
+ const length = parseFloat(mafs.vec.dist(start, end).toFixed(1));
28934
+ return /*#__PURE__*/React__namespace.createElement(TextLabel, {
28935
+ key: "side-" + i,
28936
+ x: x,
28937
+ y: y
28938
+ }, !Number.isInteger(length) && "≈ ", length);
28883
28939
  }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28884
28940
  points: [...points],
28885
28941
  color: "transparent",
@@ -28899,11 +28955,73 @@ const PolygonGraph = props => {
28899
28955
  }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
28900
28956
  key: "point-" + i,
28901
28957
  point: point,
28902
- onMove: destination => dispatch(moveControlPoint(i, destination)),
28958
+ onMove: destination => dispatch(movePoint(i, destination)),
28903
28959
  "data-testid": type + i
28904
28960
  })));
28905
28961
  };
28906
28962
 
28963
+ function PointGraph(props) {
28964
+ const {
28965
+ dispatch
28966
+ } = props;
28967
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
28968
+ key: i,
28969
+ point: point,
28970
+ onMove: destination => dispatch(movePoint(i, destination))
28971
+ })));
28972
+ }
28973
+
28974
+ const GraphStateContext = /*#__PURE__*/React.createContext({
28975
+ state: {},
28976
+ dispatch: () => {}
28977
+ });
28978
+ function useGraphState() {
28979
+ return React__default["default"].useContext(GraphStateContext);
28980
+ }
28981
+
28982
+ function Arrow(props) {
28983
+ const {
28984
+ userTransform,
28985
+ viewTransform
28986
+ } = mafs.useTransformContext();
28987
+ const point = [props.x, props.y];
28988
+ const userTransformedPoint = mafs.vec.transform(point, userTransform);
28989
+ const viewTransformedPoint = mafs.vec.transform(userTransformedPoint, viewTransform);
28990
+ return /*#__PURE__*/React__default["default"].createElement("g", {
28991
+ transform: `translate(${viewTransformedPoint[0]} ${viewTransformedPoint[1]}) rotate(${props.rotate}) scale(1.25)`
28992
+ }, /*#__PURE__*/React__default["default"].createElement("g", {
28993
+ transform: "translate(-1)"
28994
+ }, /*#__PURE__*/React__default["default"].createElement("path", {
28995
+ d: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
28996
+ fill: "none"
28997
+ })));
28998
+ }
28999
+ function AxisArrows() {
29000
+ const {
29001
+ state
29002
+ } = useGraphState();
29003
+ const range = state.range;
29004
+ const [xMin, xMax] = range[0];
29005
+ const [yMin, yMax] = range[1];
29006
+ return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(Arrow, {
29007
+ x: xMax,
29008
+ y: 0,
29009
+ rotate: 0
29010
+ }), /*#__PURE__*/React__default["default"].createElement(Arrow, {
29011
+ x: 0,
29012
+ y: yMin,
29013
+ rotate: 90
29014
+ }), /*#__PURE__*/React__default["default"].createElement(Arrow, {
29015
+ x: xMin,
29016
+ y: 0,
29017
+ rotate: 180
29018
+ }), /*#__PURE__*/React__default["default"].createElement(Arrow, {
29019
+ x: 0,
29020
+ y: yMax,
29021
+ rotate: 270
29022
+ }));
29023
+ }
29024
+
28907
29025
  const renderLineLabel = (n, _ref) => {
28908
29026
  let [min, max] = _ref;
28909
29027
  return n !== -1 && n !== min && n !== max;
@@ -28914,10 +29032,10 @@ const axisOptions = (props, index) => ({
28914
29032
  subdivisions: props.step[index] / props.gridStep[index],
28915
29033
  labels: n => renderLineLabel(n, props.range[index]) ? n : ""
28916
29034
  });
28917
- const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
29035
+ const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
28918
29036
  xAxis: axisOptions(props, 0),
28919
29037
  yAxis: axisOptions(props, 1)
28920
- });
29038
+ }), props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(AxisArrows, null));
28921
29039
 
28922
29040
  // Displays a stylized open eye: lint warnings are visible
28923
29041
  const VisibleIcon = () => /*#__PURE__*/React__namespace.createElement("svg", {
@@ -29615,22 +29733,32 @@ const LegacyGrid = _ref => {
29615
29733
  return null;
29616
29734
  };
29617
29735
 
29618
- /** Determine if coords is type CollinearTuple[] */
29619
- const isCollinearTuples = coords => Array.isArray(coords[0][0]);
29620
-
29621
- // Generic type makes returned state match input state
29622
29736
  function interactiveGraphReducer(state, action) {
29737
+ switch (action.type) {
29738
+ case MOVE_CONTROL_POINT:
29739
+ return doMoveControlPoint(state, action);
29740
+ case MOVE_LINE:
29741
+ return doMoveLine(state, action);
29742
+ case MOVE_ALL:
29743
+ return doMoveAll(state, action);
29744
+ case MOVE_POINT:
29745
+ return doMovePoint(state, action);
29746
+ default:
29747
+ throw new wonderStuffCore.UnreachableCaseError(action);
29748
+ }
29749
+ }
29750
+ function doMoveControlPoint(state, action) {
29623
29751
  const {
29624
29752
  snapStep,
29625
29753
  range
29626
29754
  } = state;
29627
- if (!state.coords) {
29628
- throw new Error("Graph state must have been initialized with coords");
29629
- }
29630
- switch (action.type) {
29631
- case MOVE_CONTROL_POINT:
29755
+ switch (state.type) {
29756
+ case "segment":
29757
+ case "linear":
29758
+ case "linear-system":
29759
+ case "ray":
29632
29760
  {
29633
- const newCoords = action.itemIndex !== undefined && state.coords && isCollinearTuples(state.coords) ? updateAtIndex({
29761
+ const newCoords = updateAtIndex({
29634
29762
  array: state.coords,
29635
29763
  index: action.itemIndex,
29636
29764
  update: tuple => setAtIndex({
@@ -29645,23 +29773,9 @@ function interactiveGraphReducer(state, action) {
29645
29773
  })
29646
29774
  })
29647
29775
  })
29648
- }) : setAtIndex({
29649
- array: state.coords,
29650
- index: action.pointIndex,
29651
- newValue: snap({
29652
- snapStep,
29653
- point: bound$1({
29654
- snapStep,
29655
- range,
29656
- point: action.destination
29657
- })
29658
- })
29659
29776
  });
29660
-
29661
- // Cannot type narrow both conditions within function parameters,
29662
- // so this may seem redundant, but it's necessary for type safety.
29663
- const coordsToCheck = isCollinearTuples(newCoords) && action.itemIndex !== undefined ? newCoords[action.itemIndex] : newCoords;
29664
- if (!isCollinearTuples(coordsToCheck) && coordsOverlap(coordsToCheck)) {
29777
+ const coordsToCheck = newCoords[action.itemIndex];
29778
+ if (coordsOverlap(coordsToCheck)) {
29665
29779
  return state;
29666
29780
  }
29667
29781
  return {
@@ -29670,15 +29784,30 @@ function interactiveGraphReducer(state, action) {
29670
29784
  coords: newCoords
29671
29785
  };
29672
29786
  }
29673
- case MOVE_LINE:
29787
+ case "circle":
29788
+ throw new Error("FIXME implement circle reducer");
29789
+ case "point":
29790
+ case "polygon":
29791
+ throw new Error(`Don't use moveControlPoint for ${state.type} graphs. Use movePoint instead!`);
29792
+ default:
29793
+ throw new wonderStuffCore.UnreachableCaseError(state);
29794
+ }
29795
+ }
29796
+ function doMoveLine(state, action) {
29797
+ const {
29798
+ snapStep,
29799
+ range
29800
+ } = state;
29801
+ switch (state.type) {
29802
+ case "segment":
29803
+ case "linear":
29804
+ case "linear-system":
29805
+ case "ray":
29674
29806
  {
29675
- if (!isCollinearTuples(state.coords)) {
29676
- throw new Error("Cannot call this action unless coords is array of CollinearTuple.");
29677
- }
29678
29807
  if (action.itemIndex === undefined) {
29679
29808
  throw new Error("Please provide index of line to move");
29680
29809
  }
29681
- const currentLine = state.coords?.[action.itemIndex];
29810
+ const currentLine = state.coords[action.itemIndex];
29682
29811
  if (!currentLine) {
29683
29812
  throw new Error("No line to move");
29684
29813
  }
@@ -29700,15 +29829,25 @@ function interactiveGraphReducer(state, action) {
29700
29829
  });
29701
29830
  return {
29702
29831
  ...state,
29832
+ type: state.type,
29703
29833
  hasBeenInteractedWith: true,
29704
29834
  coords: newCoords
29705
29835
  };
29706
29836
  }
29707
- case MOVE_ALL:
29837
+ default:
29838
+ // The MoveLine action doesn't make sense for other graph types;
29839
+ // ignore it if it somehow happens
29840
+ return state;
29841
+ }
29842
+ }
29843
+ function doMoveAll(state, action) {
29844
+ const {
29845
+ snapStep,
29846
+ range
29847
+ } = state;
29848
+ switch (state.type) {
29849
+ case "polygon":
29708
29850
  {
29709
- if (isCollinearTuples(state.coords)) {
29710
- throw new Error("Cannot call this action unless coords is array of vectors.");
29711
- }
29712
29851
  const change = getChange(state.coords, action.delta, {
29713
29852
  snapStep,
29714
29853
  range
@@ -29724,7 +29863,34 @@ function interactiveGraphReducer(state, action) {
29724
29863
  };
29725
29864
  }
29726
29865
  default:
29727
- throw new wonderStuffCore.UnreachableCaseError(action);
29866
+ // MoveAll is not supported for other state types; just ignore it.
29867
+ return state;
29868
+ }
29869
+ }
29870
+ function doMovePoint(state, action) {
29871
+ switch (state.type) {
29872
+ case "point":
29873
+ case "polygon":
29874
+ {
29875
+ return {
29876
+ ...state,
29877
+ hasBeenInteractedWith: true,
29878
+ coords: setAtIndex({
29879
+ array: state.coords,
29880
+ index: action.index,
29881
+ newValue: snap({
29882
+ snapStep: state.snapStep,
29883
+ point: bound$1({
29884
+ snapStep: state.snapStep,
29885
+ range: state.range,
29886
+ point: action.destination
29887
+ })
29888
+ })
29889
+ })
29890
+ };
29891
+ }
29892
+ default:
29893
+ throw new Error("The movePoint action is only for point and polygon graphs");
29728
29894
  }
29729
29895
  }
29730
29896
  const getChange = (coords, delta, constraintOpts) => {
@@ -29811,7 +29977,7 @@ function clamp(value, min, max) {
29811
29977
  const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && kmath.vector.equal(coord, c)));
29812
29978
  function updateAtIndex(args) {
29813
29979
  const {
29814
- array = [],
29980
+ array,
29815
29981
  index,
29816
29982
  update
29817
29983
  } = args;
@@ -29824,13 +29990,12 @@ function updateAtIndex(args) {
29824
29990
  }
29825
29991
  function setAtIndex(args) {
29826
29992
  const {
29827
- array = [],
29993
+ array,
29828
29994
  index,
29829
29995
  newValue
29830
29996
  } = args;
29831
29997
  const copy = [...array];
29832
29998
  copy[index] = newValue;
29833
- // restoring readonly to array
29834
29999
  return copy;
29835
30000
  }
29836
30001
 
@@ -29872,18 +30037,31 @@ function initializeGraphState(params) {
29872
30037
  };
29873
30038
  case "polygon":
29874
30039
  return {
30040
+ type: "polygon",
29875
30041
  hasBeenInteractedWith: false,
29876
30042
  range,
29877
30043
  snapStep,
29878
- ...graph,
30044
+ showAngles: Boolean(graph.showAngles),
30045
+ showSides: Boolean(graph.showSides),
29879
30046
  coords: getPolygonCoords({
29880
30047
  graph,
29881
30048
  range,
29882
30049
  step
29883
30050
  })
29884
30051
  };
29885
- case "angle":
29886
30052
  case "point":
30053
+ return {
30054
+ type: graph.type,
30055
+ hasBeenInteractedWith: false,
30056
+ range,
30057
+ snapStep,
30058
+ coords: getDefaultPoints({
30059
+ graph,
30060
+ step,
30061
+ range
30062
+ })
30063
+ };
30064
+ case "angle":
29887
30065
  case "circle":
29888
30066
  case "sinusoid":
29889
30067
  case "quadratic":
@@ -29892,43 +30070,99 @@ function initializeGraphState(params) {
29892
30070
  throw new wonderStuffCore.UnreachableCaseError(graph);
29893
30071
  }
29894
30072
  }
30073
+ const getDefaultPoints = _ref => {
30074
+ let {
30075
+ graph,
30076
+ range,
30077
+ step
30078
+ } = _ref;
30079
+ const numPoints = graph.numPoints || 1;
30080
+ let coords = graph.coords?.slice();
30081
+ if (coords) {
30082
+ return coords;
30083
+ }
30084
+ switch (numPoints) {
30085
+ case 1:
30086
+ // Back in the day, one point's coords were in graph.coord
30087
+ coords = [graph.coord || [0, 0]];
30088
+ break;
30089
+ case 2:
30090
+ coords = [[-5, 0], [5, 0]];
30091
+ break;
30092
+ case 3:
30093
+ coords = [[-5, 0], [0, 0], [5, 0]];
30094
+ break;
30095
+ case 4:
30096
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
30097
+ break;
30098
+ case 5:
30099
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
30100
+ break;
30101
+ case 6:
30102
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
30103
+ break;
30104
+ default:
30105
+ coords = [];
30106
+ break;
30107
+ }
30108
+ // Transform coords from their -10 to 10 space to 0 to 1
30109
+ // because of the old graph.coord, and also it's easier.
30110
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30111
+ return normalizePoints(range, step, newCoords);
30112
+ };
29895
30113
 
29896
30114
  // TS v4 doesn't narrow return types, while v5 does.
29897
30115
  // Instead of updating to v5, using generic type to relate input and output types.
29898
30116
  function getGradableGraph(state, initialGraph) {
29899
30117
  if (!state.hasBeenInteractedWith) {
29900
- return initialGraph;
30118
+ return {
30119
+ type: initialGraph.type
30120
+ };
29901
30121
  }
29902
- switch (true) {
29903
- // coords: Array of CollinearTuple
29904
- case state.type === "linear-system" && initialGraph.type === "linear-system":
29905
- case state.type === "segment" && initialGraph.type === "segment":
29906
- return {
29907
- ...initialGraph,
29908
- coords: state.coords
29909
- };
29910
- // coords: CollinearTuple
29911
- case state.type === "linear" && initialGraph.type === "linear":
29912
- case state.type === "ray" && initialGraph.type === "ray":
29913
- return {
29914
- ...initialGraph,
29915
- coords: state.coords?.[0]
29916
- };
29917
- case state.type === "polygon" && initialGraph.type === "polygon":
29918
- return {
29919
- ...initialGraph,
29920
- coords: state.coords
29921
- };
29922
- default:
29923
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
30122
+ if (initialGraph.type === "linear-system" && state.type === "linear-system") {
30123
+ return {
30124
+ ...initialGraph,
30125
+ coords: state.coords
30126
+ };
30127
+ }
30128
+ if (state.type === "segment" && initialGraph.type === "segment") {
30129
+ return {
30130
+ ...initialGraph,
30131
+ coords: state.coords
30132
+ };
30133
+ }
30134
+ if (state.type === "linear" && initialGraph.type === "linear") {
30135
+ return {
30136
+ ...initialGraph,
30137
+ coords: state.coords[0]
30138
+ };
30139
+ }
30140
+ if (state.type === "ray" && initialGraph.type === "ray") {
30141
+ return {
30142
+ ...initialGraph,
30143
+ coords: state.coords[0]
30144
+ };
30145
+ }
30146
+ if (state.type === "polygon" && initialGraph.type === "polygon") {
30147
+ return {
30148
+ ...initialGraph,
30149
+ coords: state.coords
30150
+ };
29924
30151
  }
30152
+ if (state.type === "point" && initialGraph.type === "point") {
30153
+ return {
30154
+ ...initialGraph,
30155
+ coords: state.coords
30156
+ };
30157
+ }
30158
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29925
30159
  }
29926
- const getDefaultSegments = _ref => {
30160
+ const getDefaultSegments = _ref2 => {
29927
30161
  let {
29928
30162
  graph,
29929
30163
  range,
29930
30164
  step
29931
- } = _ref;
30165
+ } = _ref2;
29932
30166
  const ys = n => {
29933
30167
  switch (n) {
29934
30168
  case 2:
@@ -29954,24 +30188,24 @@ const getDefaultSegments = _ref => {
29954
30188
  });
29955
30189
  };
29956
30190
  const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
29957
- const getLineCoords = _ref2 => {
30191
+ const getLineCoords = _ref3 => {
29958
30192
  let {
29959
30193
  graph,
29960
30194
  range,
29961
30195
  step
29962
- } = _ref2;
30196
+ } = _ref3;
29963
30197
  return (
29964
30198
  // Return two lines for a linear system, one for a ray or linear
29965
30199
  graph.coords ?? graph.type === "linear-system" ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
29966
30200
  );
29967
30201
  };
29968
- const getPolygonCoords = _ref3 => {
30202
+ const getPolygonCoords = _ref4 => {
29969
30203
  let {
29970
30204
  graph,
29971
30205
  range,
29972
30206
  step
29973
- } = _ref3;
29974
- let coords = graph.coords;
30207
+ } = _ref4;
30208
+ let coords = graph.coords?.slice();
29975
30209
  if (coords) {
29976
30210
  return coords;
29977
30211
  }
@@ -30025,6 +30259,13 @@ const renderGraph = props => {
30025
30259
  graphState: state,
30026
30260
  dispatch: dispatch
30027
30261
  });
30262
+ case "point":
30263
+ return /*#__PURE__*/React__namespace.createElement(PointGraph, {
30264
+ graphState: state,
30265
+ dispatch: dispatch
30266
+ });
30267
+ case "circle":
30268
+ throw new Error("the circle graph type is not yet implemented");
30028
30269
  default:
30029
30270
  return new wonderStuffCore.UnreachableCaseError(type);
30030
30271
  }
@@ -30032,10 +30273,24 @@ const renderGraph = props => {
30032
30273
  const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
30033
30274
  const [width, height] = props.box;
30034
30275
  const [state, dispatch] = React__namespace.useReducer(interactiveGraphReducer, props, initializeGraphState);
30276
+ const prevState = React.useRef(state);
30277
+ React.useEffect(() => {
30278
+ if (prevState.current !== state) {
30279
+ props.onChange({
30280
+ graph: state
30281
+ });
30282
+ }
30283
+ prevState.current = state;
30284
+ }, [props, state]);
30035
30285
  React__namespace.useImperativeHandle(ref, () => ({
30036
30286
  getUserInput: () => getGradableGraph(state, props.graph)
30037
30287
  }));
30038
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
30288
+ return /*#__PURE__*/React__namespace.createElement(GraphStateContext.Provider, {
30289
+ value: {
30290
+ state,
30291
+ dispatch
30292
+ }
30293
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
30039
30294
  style: {
30040
30295
  width,
30041
30296
  height,
@@ -30061,12 +30316,12 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
30061
30316
  zoom: false,
30062
30317
  width: width,
30063
30318
  height: height
30064
- }, /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30319
+ }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30065
30320
  lockedFigures: props.lockedFigures
30066
30321
  }), renderGraph({
30067
30322
  state,
30068
30323
  dispatch
30069
- }))));
30324
+ })))));
30070
30325
  });
30071
30326
 
30072
30327
  const {
@@ -31233,6 +31488,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
31233
31488
  });
31234
31489
  $__default["default"](this.angle).on("move", () => {
31235
31490
  this.onChange({
31491
+ // @ts-expect-error Type '{ coords: any; type: "angle"; showAngles?: boolean | undefined; allowReflexAngles?: boolean | undefined; angleOffsetDeg?: number | undefined; snapDegrees?: number | undefined; match?: "congruent" | undefined; }' is not assignable to type 'InteractiveGraphState | undefined'.
31236
31492
  graph: {
31237
31493
  ...graph,
31238
31494
  coords: this.angle?.getClockwiseCoords()
@@ -31687,18 +31943,18 @@ class InteractiveGraph extends React__namespace.Component {
31687
31943
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
31688
31944
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
31689
31945
  }
31690
- static validate(
31691
- // TODO(kevinb): rename state to userInput
31692
- // state === userInput
31693
- state, rubric, component) {
31946
+ static validate(userInput, rubric, component) {
31694
31947
  // When nothing has moved, there will neither be coords nor the
31695
31948
  // circle's center/radius fields. When those fields are absent, skip
31696
31949
  // all these checks; just go mark the answer as empty.
31697
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
31698
- const hasValue = !!(state.coords || state.center && state.radius);
31699
- if (state.type === rubric.correct.type && hasValue) {
31700
- if (state.type === "linear" && rubric.correct.type === "linear" && state.coords != null) {
31701
- const guess = state.coords;
31950
+ const hasValue = Boolean(
31951
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
31952
+ userInput.coords ||
31953
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
31954
+ userInput.center && userInput.radius);
31955
+ if (userInput.type === rubric.correct.type && hasValue) {
31956
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
31957
+ const guess = userInput.coords;
31702
31958
  const correct = rubric.correct.coords;
31703
31959
 
31704
31960
  // If both of the guess points are on the correct line, it's
@@ -31715,8 +31971,8 @@ class InteractiveGraph extends React__namespace.Component {
31715
31971
  message: null
31716
31972
  };
31717
31973
  }
31718
- } else if (state.type === "linear-system" && rubric.correct.type === "linear-system" && state.coords != null) {
31719
- const guess = state.coords;
31974
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
31975
+ const guess = userInput.coords;
31720
31976
  const correct = rubric.correct.coords;
31721
31977
  if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
31722
31978
  return {
@@ -31726,9 +31982,9 @@ class InteractiveGraph extends React__namespace.Component {
31726
31982
  message: null
31727
31983
  };
31728
31984
  }
31729
- } else if (state.type === "quadratic" && rubric.correct.type === "quadratic" && state.coords != null) {
31985
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
31730
31986
  // If the parabola coefficients match, it's correct.
31731
- const guessCoeffs = this.getQuadraticCoefficients(state.coords);
31987
+ const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
31732
31988
  const correctCoeffs = this.getQuadraticCoefficients(
31733
31989
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31734
31990
  rubric.correct.coords);
@@ -31740,8 +31996,8 @@ class InteractiveGraph extends React__namespace.Component {
31740
31996
  message: null
31741
31997
  };
31742
31998
  }
31743
- } else if (state.type === "sinusoid" && rubric.correct.type === "sinusoid" && state.coords != null) {
31744
- const guessCoeffs = this.getSinusoidCoefficients(state.coords);
31999
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
32000
+ const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
31745
32001
  const correctCoeffs = this.getSinusoidCoefficients(
31746
32002
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31747
32003
  rubric.correct.coords);
@@ -31756,8 +32012,8 @@ class InteractiveGraph extends React__namespace.Component {
31756
32012
  message: null
31757
32013
  };
31758
32014
  }
31759
- } else if (state.type === "circle" && rubric.correct.type === "circle") {
31760
- if (deepEq$1(state.center, rubric.correct.center) && eq(state.radius, rubric.correct.radius)) {
32015
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
32016
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
31761
32017
  return {
31762
32018
  type: "points",
31763
32019
  earned: 1,
@@ -31765,9 +32021,9 @@ class InteractiveGraph extends React__namespace.Component {
31765
32021
  message: null
31766
32022
  };
31767
32023
  }
31768
- } else if (state.type === "point" && rubric.correct.type === "point" && state.coords != null) {
32024
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
31769
32025
  let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
31770
- const guess = state.coords?.slice();
32026
+ const guess = userInput.coords.slice();
31771
32027
  correct = correct.slice();
31772
32028
  // Everything's already rounded so we shouldn't need to do an
31773
32029
  // eq() comparison but _.isEqual(0, -0) is false, so we'll use
@@ -31784,8 +32040,8 @@ class InteractiveGraph extends React__namespace.Component {
31784
32040
  message: null
31785
32041
  };
31786
32042
  }
31787
- } else if (state.type === "polygon" && rubric.correct.type === "polygon" && state.coords != null) {
31788
- const guess = state.coords?.slice();
32043
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
32044
+ const guess = userInput.coords?.slice();
31789
32045
  // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
31790
32046
  const correct = rubric.correct.coords?.slice();
31791
32047
  let match;
@@ -31809,8 +32065,8 @@ class InteractiveGraph extends React__namespace.Component {
31809
32065
  message: null
31810
32066
  };
31811
32067
  }
31812
- } else if (state.type === "segment" && rubric.correct.type === "segment" && state.coords != null) {
31813
- let guess = state.coords.slice();
32068
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
32069
+ let guess = userInput.coords.slice();
31814
32070
  let correct = rubric.correct.coords?.slice();
31815
32071
  guess = ___default["default"].invoke(guess, "sort").sort();
31816
32072
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
@@ -31823,8 +32079,8 @@ class InteractiveGraph extends React__namespace.Component {
31823
32079
  message: null
31824
32080
  };
31825
32081
  }
31826
- } else if (state.type === "ray" && rubric.correct.type === "ray" && state.coords != null) {
31827
- const guess = state.coords;
32082
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
32083
+ const guess = userInput.coords;
31828
32084
  const correct = rubric.correct.coords;
31829
32085
  if (
31830
32086
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
@@ -31838,8 +32094,8 @@ class InteractiveGraph extends React__namespace.Component {
31838
32094
  message: null
31839
32095
  };
31840
32096
  }
31841
- } else if (state.type === "angle" && rubric.correct.type === "angle") {
31842
- const guess = state.coords;
32097
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
32098
+ const guess = userInput.coords;
31843
32099
  const correct = rubric.correct.coords;
31844
32100
  let match;
31845
32101
  if (rubric.correct.match === "congruent") {
@@ -31878,7 +32134,7 @@ class InteractiveGraph extends React__namespace.Component {
31878
32134
 
31879
32135
  // The input wasn't correct, so check if it's a blank input or if it's
31880
32136
  // actually just wrong
31881
- if (!hasValue || ___default["default"].isEqual(state, rubric.graph)) {
32137
+ if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
31882
32138
  // We're where we started.
31883
32139
  return {
31884
32140
  type: "invalid",
@@ -42073,7 +42329,7 @@ const init = function (options) {
42073
42329
 
42074
42330
  // This file is processed by a Rollup plugin (replace) to inject the production
42075
42331
  const libName = "@khanacademy/perseus";
42076
- const libVersion = "20.8.1";
42332
+ const libVersion = "21.1.0";
42077
42333
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
42078
42334
 
42079
42335
  const apiVersion = {
@@ -44322,6 +44578,138 @@ const registerAllWidgetsForTesting = () => {
44322
44578
  replaceDeprecatedWidgets();
44323
44579
  };
44324
44580
 
44581
+ // These are all the current widgets that require user-input and are supported for automatic scoring
44582
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44583
+
44584
+ /**
44585
+ * Regex for widget placeholders in a string.
44586
+ *
44587
+ * First capture group is the widget ID (ex. 'radio 1')
44588
+ * Second capture group is the widget type (ex. "radio)
44589
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44590
+ */
44591
+ function getWidgetRegex() {
44592
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
44593
+ }
44594
+
44595
+ /**
44596
+ * Add a widget placeholder using the widget ID.
44597
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
44598
+ *
44599
+ * @param {string} id
44600
+ * @returns {string}
44601
+ */
44602
+ function addWidget(id) {
44603
+ return `[[☃ ${id}]]`;
44604
+ }
44605
+
44606
+ /**
44607
+ * Get a widget type by a widget's ID
44608
+ * (please don't derive type from ID)
44609
+ *
44610
+ * @param {string} widgetId the ID of the widget
44611
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44612
+ * @returns {string} the widget type (ie "radio")
44613
+ */
44614
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44615
+ const widget = widgetMap[WidgetId];
44616
+ return widget?.type ?? null;
44617
+ }
44618
+
44619
+ /**
44620
+ * Does the content have a specific type of widget?
44621
+ *
44622
+ * @param {string} type the type of the widget in question (ie "radio")
44623
+ * @param {string} content the string to search through
44624
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44625
+ * @returns {boolean} if the content includes the widget type
44626
+ */
44627
+ function contentHasWidgetType(type, content, widgetMap) {
44628
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
44629
+ }
44630
+
44631
+ /**
44632
+ * Extract all widget IDs, which includes the widget type and instance number.
44633
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44634
+ *
44635
+ * Content should contain Perseus widget placeholders,
44636
+ * which look like: '[[☃ radio 1]]'.
44637
+ *
44638
+ * @param {string} content
44639
+ * @returns {ReadonlyArray<string>} widgetIds
44640
+ */
44641
+ function getWidgetIdsFromContent(content) {
44642
+ const widgets = [];
44643
+ const localWidgetRegex = getWidgetRegex();
44644
+ let match = localWidgetRegex.exec(content);
44645
+ while (match !== null) {
44646
+ widgets.push(match[1]);
44647
+ match = localWidgetRegex.exec(content);
44648
+ }
44649
+ return widgets;
44650
+ }
44651
+
44652
+ /**
44653
+ * Get a list of widget IDs from content,
44654
+ * but only for specific widget types
44655
+ *
44656
+ * @param {string} type the type of widget (ie "radio")
44657
+ * @param {string} content the string to parse
44658
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44659
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
44660
+ */
44661
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
44662
+ const rv = [];
44663
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
44664
+ widgetIdsInContent.forEach(widgetId => {
44665
+ const widget = widgetMap[widgetId];
44666
+ if (widget?.type === type) {
44667
+ rv.push(widgetId);
44668
+ }
44669
+ });
44670
+ return rv;
44671
+ }
44672
+
44673
+ /**
44674
+ * Pull the widget map out of ItemData
44675
+ *
44676
+ * @param {PerseusItem} PerseusItem containing a widgetMap
44677
+ * @returns {WidgetMap} the widget map in the PerseusItem
44678
+ */
44679
+ function getWidgetsMapFromItemData(itemData) {
44680
+ return itemData.question.widgets;
44681
+ }
44682
+
44683
+ /**
44684
+ * Get the widget information from a WidgetMap
44685
+ *
44686
+ * @param {string} widgetId the ID of the widget
44687
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
44688
+ */
44689
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
44690
+ return widgetMap[widgetId] ?? null;
44691
+ }
44692
+
44693
+ /**
44694
+ * Get select widgets from a widget map.
44695
+ * Useful for multi-items that needs to split
44696
+ * one PerseusItem into several
44697
+ *
44698
+ * @param {ReadonlyArray<string>} widgetIds to extract
44699
+ * @param {PerseusWidgetsMap} widgetMap to extract from
44700
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
44701
+ */
44702
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
44703
+ const widgets = {};
44704
+ widgetIds.forEach(widgetId => {
44705
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
44706
+ if (widget) {
44707
+ widgets[widgetId] = widget;
44708
+ }
44709
+ });
44710
+ return widgets;
44711
+ }
44712
+
44325
44713
  /**
44326
44714
  * This function extracts the answers from the widgets.
44327
44715
  *
@@ -44443,7 +44831,7 @@ function getAnswersFromWidgets(widgets) {
44443
44831
  // E.g. 'item 1\nitem 2\nitem 3'
44444
44832
  const orderer = widget;
44445
44833
  if (orderer.options?.correctOptions) {
44446
- answers.push(orderer.options.correctOptions.map(option => option.content).join("\n"));
44834
+ answers.push(joinOptionContents(orderer.options.correctOptions));
44447
44835
  }
44448
44836
  break;
44449
44837
  case "sorter":
@@ -44502,6 +44890,31 @@ function getAnswersFromWidgets(widgets) {
44502
44890
  return answers;
44503
44891
  }
44504
44892
 
44893
+ /**
44894
+ * Join the content of the options together.
44895
+ * @param {Array<{content: string}>} options
44896
+ * @returns {string}
44897
+ */
44898
+ const joinOptionContents = options => options.map(_ref => {
44899
+ let {
44900
+ content
44901
+ } = _ref;
44902
+ return content;
44903
+ }).join("\n");
44904
+
44905
+ /**
44906
+ * Convert an index to an option letter.
44907
+ * @param {number} index
44908
+ * @returns {string}
44909
+ * @example
44910
+ * toOptionLetter(0) // 'A'
44911
+ * toOptionLetter(1) // 'B'
44912
+ * toOptionLetter(25) // 'Z'
44913
+ * // Once the index goes past 25, it will start returning special characters
44914
+ * toOptionLetter(26) // '['
44915
+ */
44916
+ const toOptionLetter = index => String.fromCharCode("A".charCodeAt(0) + index);
44917
+
44505
44918
  /**
44506
44919
  * Inject a string equivalent of the widgets into the content.
44507
44920
  *
@@ -44512,7 +44925,7 @@ function getAnswersFromWidgets(widgets) {
44512
44925
  * @param {PerseusRenderer["widgets"]} widgets
44513
44926
  * @returns
44514
44927
  */
44515
- function injectWidgets(content, widgets) {
44928
+ function injectWidgets(content, widgets, widgetProps) {
44516
44929
  // The types for taskProgress.itemData are not well defined,
44517
44930
  // so there is a chance that widgets or content could be undefined.
44518
44931
  // In that scenario, simply exit early and return the content as is.
@@ -44531,11 +44944,22 @@ function injectWidgets(content, widgets) {
44531
44944
  switch (widget.type) {
44532
44945
  case "radio":
44533
44946
  // Replace radio with the radio options
44534
- // '[[☃ Radio 1]]' -> 'option 1\noption 2\noption 3'
44947
+ // '[[☃ Radio 1]]' -> 'choice 1\nchoice 2\nchoice 3'
44948
+ // or if the current widget state is available with proper order,
44949
+ // '[[☃ Radio 1]]' ->
44950
+ // 'Option A: choice 1\nOption B: choice 2\nOption C: choice 3'
44535
44951
  const radio = widget;
44952
+ const radioProps = widgetProps?.[widgetID];
44536
44953
  if (radio.options?.choices?.length) {
44537
- let radioContext = radio.options.choices.map(choice => choice.content).join("\n");
44538
- if (radio.options?.randomize) {
44954
+ let radioContext = joinOptionContents(radioProps ? radioProps.choices.map((_ref2, i) => {
44955
+ let {
44956
+ content
44957
+ } = _ref2;
44958
+ return {
44959
+ content: `Option ${toOptionLetter(i)}: ${content}`
44960
+ };
44961
+ }) : radio.options.choices);
44962
+ if (!radioProps && radio.options?.randomize) {
44539
44963
  radioContext += "\nThose options are displayed in a different order to the user. If the user says the letter, number, or ordinal number, always ask them clarify which option they are referring to.\n";
44540
44964
  }
44541
44965
  context = context.replace(`[[☃ ${widgetID}]]`, radioContext);
@@ -44643,7 +45067,7 @@ function injectWidgets(content, widgets) {
44643
45067
  // '[[☃ Orderer 1]]' -> 'option 1\noption 2\noption 3'
44644
45068
  const orderer = widget;
44645
45069
  if (orderer.options?.options) {
44646
- context = context.replace(`[[☃ ${widgetID}]]`, orderer.options.options.map(option => option.content).join("\n"));
45070
+ context = context.replace(`[[☃ ${widgetID}]]`, joinOptionContents(orderer.options.options));
44647
45071
  }
44648
45072
  break;
44649
45073
  case "sorter":
@@ -44726,18 +45150,15 @@ const INDIVIDUAL_ANSWER_WIDGETS = ["interactive-graph", "categorizer", "grapher"
44726
45150
 
44727
45151
  /* Widgets that are supported for automatic scoring */
44728
45152
  const SUPPORTED_WIDGETS = ["radio", "numeric-input", "input-number", "expression", ...INDIVIDUAL_ANSWER_WIDGETS];
44729
- const getWidgetTypeFromWidgetKey = widgetKey => {
44730
- return widgetKey.split(" ")[0];
44731
- };
44732
45153
 
44733
45154
  /* Verify if the perseus item has supported widgets for automatic scoring */
44734
- const isWrongAnswerSupported = widgetKeys => {
44735
- return widgetKeys.length !== 0 && widgetKeys.every(widgetKey => SUPPORTED_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey)));
45155
+ const isWrongAnswerSupported = (widgetIds, widgetMap) => {
45156
+ return widgetIds.length !== 0 && widgetIds.every(widgetId => SUPPORTED_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap)));
44736
45157
  };
44737
45158
 
44738
- /* Verify if the widget key has an individual answer for the coach report view */
44739
- const shouldHaveIndividualAnswer = widgetKey => {
44740
- return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey));
45159
+ /* Verify if the widget ID has an individual answer for the coach report view */
45160
+ const shouldHaveIndividualAnswer = (widgetId, widgetMap) => {
45161
+ return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap));
44741
45162
  };
44742
45163
 
44743
45164
  /* Returns the answer userInput submission for currently supported widgets */
@@ -44756,29 +45177,30 @@ const getAnswerFromUserInput = (widgetType, userInput) => {
44756
45177
  return userInput;
44757
45178
  };
44758
45179
 
44759
- /* Returns the correct answer for a given widget key and Perseus Item */
45180
+ /* Returns the correct answer for a given widget ID and Perseus Item */
44760
45181
  // TODO (LEMS-1835): We should fix the resonse type from getWidget to be specific.
44761
45182
  // TODO (LEMS-1836): We should also consider adding the getOneCorrectAnswerFromRubric method to all widgets.
44762
- const getCorrectAnswerForWidgetKey = (widgetKey, itemData) => {
44763
- const rubric = itemData.question.widgets[widgetKey].options;
44764
- const widgetType = getWidgetTypeFromWidgetKey(widgetKey);
45183
+ const getCorrectAnswerForWidgetId = (widgetId, itemData) => {
45184
+ const rubric = itemData.question.widgets[widgetId].options;
45185
+ const widgetMap = getWidgetsMapFromItemData(itemData);
45186
+ const widgetType = getWidgetTypeByWidgetId(widgetId, widgetMap);
44765
45187
  const widget = getWidget(widgetType);
44766
45188
 
44767
45189
  // @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType<any>'.
44768
45190
  return widget?.getOneCorrectAnswerFromRubric?.(rubric);
44769
45191
  };
44770
45192
 
44771
- /* Verify if the widget key exists in the content string of the Perseus Item */
44772
- const isWidgetKeyInContent = (perseusItem, widgetKey) => {
44773
- return perseusItem.question.content.indexOf(widgetKey) !== -1;
45193
+ /* Verify if the widget ID exists in the content string of the Perseus Item */
45194
+ const isWidgetIdInContent = (perseusItem, widgetId) => {
45195
+ return perseusItem.question.content.indexOf(widgetId) !== -1;
44774
45196
  };
44775
45197
 
44776
- /* Return an array of all the widget keys that exist in the content string of a Perseus Item */
44777
- const getValidWidgetKeys = perseusItem => {
45198
+ /* Return an array of all the widget IDs that exist in the content string of a Perseus Item */
45199
+ const getValidWidgetIds = perseusItem => {
44778
45200
  const {
44779
45201
  widgets
44780
45202
  } = perseusItem.question;
44781
- return wonderStuffCore.keys(widgets).filter(key => isWidgetKeyInContent(perseusItem, key));
45203
+ return wonderStuffCore.keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
44782
45204
  };
44783
45205
 
44784
45206
  /**
@@ -44826,7 +45248,7 @@ const genericPerseusItemData = {
44826
45248
  * uses a basic Perseus object and updates it with custom values as needed.
44827
45249
  *
44828
45250
  * @param {Partial<PerseusItem>} customFields
44829
- * @returns PerseusItem
45251
+ * @returns {PerseusItem}
44830
45252
  */
44831
45253
  function generateTestPerseusItem() {
44832
45254
  let customFields = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -44836,6 +45258,107 @@ function generateTestPerseusItem() {
44836
45258
  };
44837
45259
  }
44838
45260
 
45261
+ /**
45262
+ * Creates an object with the bare amount
45263
+ * of data to be a properly typed RadioWidget
45264
+ *
45265
+ * @returns {RadioWidget} skeleton RadioWidget for testing
45266
+ */
45267
+ function generateTestRadioWidget() {
45268
+ return {
45269
+ type: "radio",
45270
+ options: {
45271
+ choices: []
45272
+ }
45273
+ };
45274
+ }
45275
+
45276
+ /**
45277
+ * Creates an object with the bare amount
45278
+ * of data to be a properly typed InteractiveGraphWidget
45279
+ *
45280
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
45281
+ */
45282
+ function generateTestInteractiveGraphWidget() {
45283
+ return {
45284
+ type: "interactive-graph",
45285
+ options: {
45286
+ step: [1, 1],
45287
+ gridStep: [1, 1],
45288
+ snapStep: [1, 1],
45289
+ markings: "graph",
45290
+ labels: ["x", "y"],
45291
+ showProtractor: false,
45292
+ showRuler: false,
45293
+ rulerLabel: "mm",
45294
+ rulerTicks: 2,
45295
+ range: [[-10, 10], [-10, 10]],
45296
+ graph: {
45297
+ type: "angle"
45298
+ },
45299
+ correct: {
45300
+ type: "angle"
45301
+ }
45302
+ }
45303
+ };
45304
+ }
45305
+
45306
+ /**
45307
+ * Creates an object with the bare amount
45308
+ * of data to be a properly typed InteractiveGraphWidget
45309
+ *
45310
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
45311
+ */
45312
+ function generateTestCategorizerWidget() {
45313
+ return {
45314
+ type: "categorizer",
45315
+ options: {
45316
+ items: [],
45317
+ categories: [],
45318
+ randomizeItems: false,
45319
+ static: false,
45320
+ values: []
45321
+ }
45322
+ };
45323
+ }
45324
+
45325
+ /**
45326
+ * Creates an object with the bare amount
45327
+ * of data to be a properly typed ExpressionWidget
45328
+ *
45329
+ * @returns {ExpressionWidget} skeleton ExpressionWidget for testing
45330
+ */
45331
+ function generateTestExpressionWidget() {
45332
+ return {
45333
+ type: "expression",
45334
+ options: {
45335
+ answerForms: [],
45336
+ buttonSets: ["basic"],
45337
+ functions: [],
45338
+ times: false
45339
+ }
45340
+ };
45341
+ }
45342
+
45343
+ /**
45344
+ * Creates an object with the bare amount
45345
+ * of data to be a properly typed NumericInputWidget
45346
+ *
45347
+ * @returns {NumericInputWidget} skeleton NumericInputWidget for testing
45348
+ */
45349
+ function generateTestNumericInputWidget() {
45350
+ return {
45351
+ type: "numeric-input",
45352
+ options: {
45353
+ answers: [],
45354
+ labelText: "",
45355
+ size: "normal",
45356
+ coefficient: false,
45357
+ static: false
45358
+ }
45359
+ };
45360
+ }
45361
+
44839
45362
  /**
44840
45363
  * Some helpers for CEDAR logging. CEDAR convention dictates
44841
45364
  * that enum values should be upper case. Ex: for the
@@ -44890,162 +45413,6 @@ function convertWidgetNameToEnum(name) {
44890
45413
  }
44891
45414
  return widgetEnum;
44892
45415
  }
44893
- function getWidgetTypeFromWidgetId(widgetId) {
44894
- const [widgetType, widgetNumber] = widgetId.split(" ");
44895
- if (!widgetNumber) {
44896
- throw new Error(`Incorrect format for widget ID: ${widgetId}`);
44897
- }
44898
- if (!widgetNameToEnum[widgetType]) {
44899
- throw new Error(`Unknown widget type (${widgetType}) in widget ID: ${widgetId}`);
44900
- }
44901
- return widgetType;
44902
- }
44903
-
44904
- let WidgetType = /*#__PURE__*/function (WidgetType) {
44905
- WidgetType["Categorizer"] = "categorizer";
44906
- WidgetType["CSProgram"] = "cs-program";
44907
- WidgetType["Definition"] = "definition";
44908
- WidgetType["Dropdown"] = "dropdown";
44909
- WidgetType["Explanation"] = "explanation";
44910
- WidgetType["Expression"] = "expression";
44911
- WidgetType["GradedGroup"] = "graded-group";
44912
- WidgetType["GradedGroupAnswerBar"] = "graded-group-answer-bar";
44913
- WidgetType["GradedGroupSet"] = "graded-group-set";
44914
- WidgetType["Grapher"] = "grapher";
44915
- WidgetType["Group"] = "group";
44916
- WidgetType["Iframe"] = "iframe";
44917
- WidgetType["Image"] = "image";
44918
- WidgetType["InputNumber"] = "input-number";
44919
- WidgetType["Interaction"] = "interaction";
44920
- WidgetType["InteractiveGraph"] = "interactive-graph";
44921
- WidgetType["InteractiveNumberLine"] = "interactive-number-line";
44922
- WidgetType["LabelImage"] = "label-image";
44923
- WidgetType["Matcher"] = "matcher";
44924
- WidgetType["Matrix"] = "matrix";
44925
- WidgetType["Measurer"] = "measurer";
44926
- WidgetType["Molecule"] = "molecule";
44927
- WidgetType["NumberLine"] = "number-line";
44928
- WidgetType["NumericInput"] = "numeric-input";
44929
- WidgetType["Orderer"] = "orderer";
44930
- WidgetType["Passage"] = "passage";
44931
- WidgetType["PassageRef"] = "passage-ref";
44932
- WidgetType["PassageRefTarget"] = "passage-ref-target";
44933
- WidgetType["Plotter"] = "plotter";
44934
- WidgetType["Protractor"] = "protractor";
44935
- WidgetType["PythonProgram"] = "python-program";
44936
- WidgetType["Radio"] = "radio";
44937
- WidgetType["Sorter"] = "sorter";
44938
- WidgetType["Table"] = "table";
44939
- WidgetType["Unit"] = "unit";
44940
- WidgetType["Video"] = "video";
44941
- WidgetType["VideoTranscriptLink"] = "video-transcript-link";
44942
- return WidgetType;
44943
- }({});
44944
- // These are all the current widgets that require user-input and are supported for automatic scoring
44945
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44946
-
44947
- /**
44948
- * Regex for widget placeholders in a string.
44949
- *
44950
- * First capture group is the widget ID (ex. 'radio 1')
44951
- * Second capture group is the widget type (ex. "radio)
44952
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44953
- */
44954
- function getWidgetRegex() {
44955
- return /\[\[☃ (([a-z-]+) \d+)\]\]/g;
44956
- }
44957
-
44958
- /**
44959
- * Add a widget placeholder using the widget ID.
44960
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
44961
- *
44962
- * @param {string} id
44963
- * @returns {string}
44964
- */
44965
- function addWidget(id) {
44966
- return `[[☃ ${id}]]`;
44967
- }
44968
-
44969
- /**
44970
- * Extract all widget IDs, which includes the widget type and instance number.
44971
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44972
- *
44973
- * Content should contain Perseus widget placeholders,
44974
- * which look like: '[[☃ radio 1]]'.
44975
- *
44976
- * @param {string} content
44977
- * @returns {Array<WidgetId>} widgets
44978
- */
44979
- function getAllWidgetIds(content) {
44980
- const widgets = [];
44981
- const localWidgetRegex = getWidgetRegex();
44982
- let match = localWidgetRegex.exec(content);
44983
- while (match !== null) {
44984
- widgets.push(match[1]);
44985
- match = localWidgetRegex.exec(content);
44986
- }
44987
- return widgets;
44988
- }
44989
-
44990
- /**
44991
- * Check if a specific widget is an actual widget type using its name.
44992
- * The widget name should include a widget type, without an instance number,
44993
- * as a string. ex. 'radio'
44994
- *
44995
- * @param {string} widgetName
44996
- * @returns {boolean}
44997
- */
44998
- function isWidgetType(widgetName) {
44999
- return Object.values(WidgetType).includes(widgetName);
45000
- }
45001
-
45002
- /**
45003
- * Extract all widget types from a Perseus JSON content string.
45004
- * This does not include the instance number and prevents duplicates.
45005
- * example output: ['radio', 'categorizer']
45006
- *
45007
- * @param {string} content
45008
- * @returns {Array<WidgetId>}
45009
- */
45010
- function getAllWidgetTypes(content) {
45011
- const widgetTypes = [];
45012
- const localWidgetRegex = getWidgetRegex();
45013
- let match = localWidgetRegex.exec(content);
45014
- while (match !== null) {
45015
- const matchType = match[2];
45016
- if (!widgetTypes.includes(matchType) && isWidgetType(matchType)) {
45017
- widgetTypes.push(matchType);
45018
- }
45019
- match = localWidgetRegex.exec(content);
45020
- }
45021
- return widgetTypes;
45022
- }
45023
-
45024
- /**
45025
- * Check if a specific widget is a question widget type using its ID.
45026
- * The widget ID includes the widget type and the instance number.
45027
- *
45028
- * @param {WidgetId} widgetId
45029
- * @returns {boolean}
45030
- */
45031
- function isQuestionWidgetType(widgetId) {
45032
- return QUESTION_WIDGETS.includes(widgetId.split(" ")[0]);
45033
- }
45034
-
45035
- /**
45036
- * Extract the widget IDs of all widgets considered question widgets. Widget IDs
45037
- * include widget type and instance number.
45038
- * example output: ['radio 1', 'categorizer 1']
45039
- *
45040
- * Content should contain Perseus widget placeholders,
45041
- * which look like: '[[☃ radio 1]]'.
45042
- *
45043
- * @param {string} content
45044
- * @returns Array<WidgetId>
45045
- */
45046
- function getQuestionWidgetIds(content) {
45047
- return getAllWidgetIds(content).filter(isQuestionWidgetType);
45048
- }
45049
45416
 
45050
45417
  const EditorJsonify = {
45051
45418
  serialize: function () {
@@ -45097,7 +45464,6 @@ exports.TableWidget = Table$1;
45097
45464
  exports.Util = Util;
45098
45465
  exports.WIDGET_PROP_DENYLIST = denylist;
45099
45466
  exports.WidgetJsonifyDeprecated = WidgetJsonifyDeprecated;
45100
- exports.WidgetType = WidgetType;
45101
45467
  exports.Widgets = widgets$1;
45102
45468
  exports.addWidget = addWidget;
45103
45469
  exports.apiVersion = apiVersion;
@@ -45105,19 +45471,26 @@ exports.buildEmptyItemTreeForShape = buildEmptyItemTreeForShape;
45105
45471
  exports.buildMapper = buildMapper;
45106
45472
  exports.components = components;
45107
45473
  exports.constants = constants;
45474
+ exports.contentHasWidgetType = contentHasWidgetType;
45108
45475
  exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
45109
45476
  exports.displaySigFigs = displaySigFigs;
45477
+ exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
45478
+ exports.generateTestExpressionWidget = generateTestExpressionWidget;
45479
+ exports.generateTestInteractiveGraphWidget = generateTestInteractiveGraphWidget;
45480
+ exports.generateTestNumericInputWidget = generateTestNumericInputWidget;
45110
45481
  exports.generateTestPerseusItem = generateTestPerseusItem;
45482
+ exports.generateTestRadioWidget = generateTestRadioWidget;
45111
45483
  exports.genericPerseusItemData = genericPerseusItemData;
45112
- exports.getAllWidgetIds = getAllWidgetIds;
45113
- exports.getAllWidgetTypes = getAllWidgetTypes;
45114
45484
  exports.getAnswerFromUserInput = getAnswerFromUserInput;
45115
45485
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
45116
- exports.getCorrectAnswerForWidgetKey = getCorrectAnswerForWidgetKey;
45117
- exports.getQuestionWidgetIds = getQuestionWidgetIds;
45118
- exports.getValidWidgetKeys = getValidWidgetKeys;
45119
- exports.getWidgetTypeFromWidgetId = getWidgetTypeFromWidgetId;
45120
- exports.getWidgetTypeFromWidgetKey = getWidgetTypeFromWidgetKey;
45486
+ exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
45487
+ exports.getValidWidgetIds = getValidWidgetIds;
45488
+ exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
45489
+ exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
45490
+ exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
45491
+ exports.getWidgetTypeByWidgetId = getWidgetTypeByWidgetId;
45492
+ exports.getWidgetsFromWidgetMap = getWidgetsFromWidgetMap;
45493
+ exports.getWidgetsMapFromItemData = getWidgetsMapFromItemData;
45121
45494
  exports.globalConstants = globalConstants;
45122
45495
  exports.globalStyles = globalStyles;
45123
45496
  exports.icons = iconPaths;
@@ -45125,7 +45498,7 @@ exports.init = init;
45125
45498
  exports.injectWidgets = injectWidgets;
45126
45499
  exports.interactiveSizes = interactiveSizes$1;
45127
45500
  exports.isItemRenderableByVersion = isItemRenderableByVersion;
45128
- exports.isWidgetKeyInContent = isWidgetKeyInContent;
45501
+ exports.isWidgetIdInContent = isWidgetIdInContent;
45129
45502
  exports.isWrongAnswerSupported = isWrongAnswerSupported;
45130
45503
  exports.itemDataVersion = itemDataVersion;
45131
45504
  exports.itemToTree = itemToTree;