@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/es/index.js CHANGED
@@ -7,7 +7,7 @@ import * as i18n from '@khanacademy/wonder-blocks-i18n';
7
7
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
8
8
  import classNames from 'classnames';
9
9
  import * as React from 'react';
10
- import React__default, { useState, useEffect, useRef } from 'react';
10
+ import React__default, { useState, useEffect, useRef, createContext } from 'react';
11
11
  import _, { debounce as debounce$1 } from 'underscore';
12
12
  import Clickable from '@khanacademy/wonder-blocks-clickable';
13
13
  import { Popover, PopoverContentCore, PopoverContent } from '@khanacademy/wonder-blocks-popover';
@@ -1921,7 +1921,7 @@ const KhanAnswerTypes = {
1921
1921
  }
1922
1922
  };
1923
1923
 
1924
- const _excluded$4 = ["scoring"];
1924
+ const _excluded$5 = ["scoring"];
1925
1925
  const ERROR_TITLE = i18n._("Oops!");
1926
1926
  const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
1927
1927
 
@@ -2009,7 +2009,7 @@ class Expression extends React.Component {
2009
2009
  let {
2010
2010
  scoring
2011
2011
  } = _ref,
2012
- rubric = _objectWithoutPropertiesLoose(_ref, _excluded$4);
2012
+ rubric = _objectWithoutPropertiesLoose(_ref, _excluded$5);
2013
2013
  const score = Expression.validate(this.getUserInput(), rubric, onInputError || function () {});
2014
2014
 
2015
2015
  // "scoring" is a flag that indicates when we are checking answers.
@@ -18832,7 +18832,7 @@ const isAccessible = widgetInfo => {
18832
18832
  }
18833
18833
  return !!accessible;
18834
18834
  };
18835
- const getAllWidgetTypes$1 = () => {
18835
+ const getAllWidgetTypes = () => {
18836
18836
  return _.keys(widgets);
18837
18837
  };
18838
18838
  const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
@@ -19101,7 +19101,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19101
19101
  getVersionVector: getVersionVector,
19102
19102
  getPublicWidgets: getPublicWidgets,
19103
19103
  isAccessible: isAccessible,
19104
- getAllWidgetTypes: getAllWidgetTypes$1,
19104
+ getAllWidgetTypes: getAllWidgetTypes,
19105
19105
  upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
19106
19106
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
19107
19107
  traverseChildWidgets: traverseChildWidgets$3,
@@ -19339,6 +19339,9 @@ class Renderer extends React.Component {
19339
19339
  if (!widgetInfo.type || !widgetInfo.alignment) {
19340
19340
  const newValues = {};
19341
19341
  if (!widgetInfo.type) {
19342
+ // TODO: why does widget have no type?
19343
+ // We don't want to derive type from widget ID
19344
+ // see: LEMS-1845
19342
19345
  newValues.type = widgetId.split(" ")[0];
19343
19346
  }
19344
19347
  if (!widgetInfo.alignment) {
@@ -21428,7 +21431,7 @@ InputWithExamples.defaultProps = {
21428
21431
  className: ""
21429
21432
  };
21430
21433
 
21431
- const _excluded$3 = ["keypadElement", "onFocus", "value"];
21434
+ const _excluded$4 = ["keypadElement", "onFocus", "value"];
21432
21435
  class SimpleKeypadInput extends React.Component {
21433
21436
  constructor(...args) {
21434
21437
  super(...args);
@@ -21467,7 +21470,7 @@ class SimpleKeypadInput extends React.Component {
21467
21470
  onFocus,
21468
21471
  value
21469
21472
  } = _this$props,
21470
- rest = _objectWithoutPropertiesLoose(_this$props, _excluded$3);
21473
+ rest = _objectWithoutPropertiesLoose(_this$props, _excluded$4);
21471
21474
  return (
21472
21475
  /*#__PURE__*/
21473
21476
  // @ts-expect-error - TS2769 - No overload matches this call.
@@ -23298,14 +23301,14 @@ var Choice$1 = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.
23298
23301
  forwardedRef: ref
23299
23302
  })));
23300
23303
 
23301
- const _excluded$2 = ["showContent", "content", "forwardedRef"];
23304
+ const _excluded$3 = ["showContent", "content", "forwardedRef"];
23302
23305
  const ChoiceNoneAbove = function ChoiceNoneAbove(props) {
23303
23306
  const {
23304
23307
  showContent,
23305
23308
  content,
23306
23309
  forwardedRef
23307
23310
  } = props,
23308
- rest = _objectWithoutPropertiesLoose(props, _excluded$2);
23311
+ rest = _objectWithoutPropertiesLoose(props, _excluded$3);
23309
23312
  const choiceProps = _extends({}, rest, {
23310
23313
  content: showContent ? content :
23311
23314
  /*#__PURE__*/
@@ -24898,9 +24901,22 @@ class Explanation extends React.Component {
24898
24901
  // Special styling is needed to fit the button in a block of text without throwing off the line spacing.
24899
24902
  // While the button is not normally included in a block of text, it needs to be able to accommodate such a case.
24900
24903
  const buttonStyleOverrides = {
24901
- height: "22px",
24904
+ height: "auto",
24902
24905
  marginLeft: "-2px",
24903
- padding: "0 2px"
24906
+ // transfer space on the left side to the padding
24907
+ marginRight: "2px",
24908
+ // adds space between the right border and any text that follows
24909
+ paddingLeft: "2px" // adds space between the left border and the button text
24910
+ };
24911
+
24912
+ const labelStyle = {
24913
+ fontSize: "18px",
24914
+ lineHeight: "20px",
24915
+ // The following property adjusts the large space between the button text and the caret icon.
24916
+ // Since we are unable to adjust the styling of the icon (where the extra space exists),
24917
+ // we are adjusting it on the text label by using a negative margin.
24918
+ marginRight: "-6px",
24919
+ "white-space": "normal"
24904
24920
  };
24905
24921
  const contentStyling = [styles$p.content, this.state.expanded ? styles$p.contentExpanded : styles$p.contentCollapsed, allowTransition && (this.state.expanded ? styles$p.transitionExpanded : styles$p.transitionCollapsed)];
24906
24922
  return /*#__PURE__*/React.createElement(UniqueIDProvider, {
@@ -24911,8 +24927,9 @@ class Explanation extends React.Component {
24911
24927
  "aria-controls": ids.get("content"),
24912
24928
  endIcon: caretIcon,
24913
24929
  kind: "tertiary",
24930
+ labelStyle: labelStyle,
24914
24931
  onClick: this._onClick,
24915
- size: "large",
24932
+ size: "small",
24916
24933
  style: buttonStyleOverrides
24917
24934
  }, promptText), /*#__PURE__*/React.createElement(View, {
24918
24935
  id: ids.get("content"),
@@ -28187,6 +28204,14 @@ const moveAll = delta => ({
28187
28204
  type: MOVE_ALL,
28188
28205
  delta
28189
28206
  });
28207
+ const MOVE_POINT = "move-point";
28208
+ function movePoint(index, destination) {
28209
+ return {
28210
+ type: MOVE_POINT,
28211
+ index,
28212
+ destination
28213
+ };
28214
+ }
28190
28215
 
28191
28216
  /**
28192
28217
  * 44 is touch best practice and AAA compliant for WCAG
@@ -28261,6 +28286,10 @@ const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
28261
28286
  return [xMax, yAtXMax];
28262
28287
  }
28263
28288
  };
28289
+ const getLines = points => points.map((point, i) => {
28290
+ const next = points[(i + 1) % points.length];
28291
+ return [point, next];
28292
+ });
28264
28293
 
28265
28294
  const defaultStroke = "var(--movable-line-stroke-color)";
28266
28295
  const MovableLine = props => {
@@ -28405,7 +28434,9 @@ const SegmentGraph = props => {
28405
28434
  onMoveLine: delta => {
28406
28435
  dispatch(moveLine(i, delta));
28407
28436
  },
28408
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28437
+ onMovePoint: (endpointIndex, destination) => {
28438
+ dispatch(moveControlPoint(endpointIndex, destination, i));
28439
+ },
28409
28440
  "data-testid": "segment" + i
28410
28441
  })));
28411
28442
  };
@@ -28521,11 +28552,52 @@ const RayGraph = props => {
28521
28552
  }));
28522
28553
  };
28523
28554
 
28555
+ /**
28556
+ * Wrapper with Mafs transform styles for SVG elements.
28557
+ * Scales and translates the SVG element to match the Mafs view.
28558
+ * Use if you do not need/want to use the hook in `use-transform.ts`.
28559
+ */
28560
+ const MafsCssTransformWrapper = ({
28561
+ children
28562
+ }) => /*#__PURE__*/React.createElement("g", {
28563
+ style: {
28564
+ transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28565
+ }
28566
+ }, children);
28567
+
28568
+ const _excluded$2 = ["children"];
28569
+ const TextLabel = _ref => {
28570
+ let {
28571
+ children
28572
+ } = _ref,
28573
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded$2);
28574
+ return /*#__PURE__*/React__default.createElement(Text$1, _extends({
28575
+ size: 16,
28576
+ svgTextProps: {
28577
+ filter: "url(#background)",
28578
+ fontWeight: "bold"
28579
+ }
28580
+ }, rest), children);
28581
+ };
28582
+ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*#__PURE__*/React__default.createElement("filter", {
28583
+ id: "background",
28584
+ x: "-5%",
28585
+ width: "110%",
28586
+ y: "0%",
28587
+ height: "100%"
28588
+ }, /*#__PURE__*/React__default.createElement("feFlood", {
28589
+ floodColor: "#FFF",
28590
+ floodOpacity: "0.64"
28591
+ }), /*#__PURE__*/React__default.createElement("feComposite", {
28592
+ operator: "over",
28593
+ in: "SourceGraphic"
28594
+ })));
28595
+
28524
28596
  const Angle = ({
28525
28597
  centerPoint,
28526
28598
  endPoints,
28527
28599
  range,
28528
- polygonPoints,
28600
+ polygonLines,
28529
28601
  showAngles
28530
28602
  }) => {
28531
28603
  const [centerX, centerY] = centerPoint;
@@ -28547,7 +28619,7 @@ const Angle = ({
28547
28619
  // Used to calculate the inside of the angle and to create square
28548
28620
  const [x3, y3] = vec.add(centerPoint, vec.add(vec.sub([x1, y1], centerPoint), vec.sub([x2, y2], centerPoint)));
28549
28621
  if (!showAngles) {
28550
- return isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleArc, {
28622
+ return isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
28551
28623
  start: [x1, y1],
28552
28624
  vertex: [x2, y2],
28553
28625
  end: [x3, y3]
@@ -28555,13 +28627,13 @@ const Angle = ({
28555
28627
  }
28556
28628
 
28557
28629
  // Midpoint betwen ends of arc
28558
- const isInside = shouldDrawArcInside([x3, y3], centerPoint, range, polygonPoints);
28559
- const largeArcFlag = isInside ? 1 : 0;
28560
- const sweepFlag = isInside ? 1 : 0;
28630
+ const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
28631
+ const largeArcFlag = isOutside ? 1 : 0;
28632
+ const sweepFlag = isOutside ? 1 : 0;
28561
28633
  const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
28562
28634
  let angleInDegrees = angle * (180 / Math.PI);
28563
28635
  // If we have triggered "largArcFlag", the angle should be greater than 180
28564
- if (isInside) {
28636
+ if (isOutside) {
28565
28637
  angleInDegrees = 360 - angleInDegrees;
28566
28638
  }
28567
28639
  const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
@@ -28578,80 +28650,54 @@ const Angle = ({
28578
28650
  }), /*#__PURE__*/React.createElement("feComposite", {
28579
28651
  operator: "over",
28580
28652
  in: "SourceGraphic"
28581
- }))), !isInside && isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleArc, {
28653
+ }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
28582
28654
  start: [x1, y1],
28583
28655
  vertex: [x2, y2],
28584
28656
  end: [x3, y3]
28585
- }) : /*#__PURE__*/React.createElement("g", {
28586
- style: {
28587
- transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28588
- }
28589
- }, /*#__PURE__*/React.createElement("path", {
28657
+ }) : /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
28590
28658
  d: arc,
28591
28659
  strokeWidth: 0.02,
28592
28660
  fill: "none"
28593
- })), /*#__PURE__*/React.createElement(Text$1, {
28661
+ })), /*#__PURE__*/React.createElement(TextLabel, {
28594
28662
  x: x3,
28595
28663
  y: y3,
28596
- size: 15,
28597
- svgTextProps: {
28598
- filter: "url(#background)",
28599
- fontWeight: "bold"
28600
- }
28601
- // Shift position if text is too close to movable point
28602
- ,
28603
28664
  attach: y3 - centerY > 0 ? "s" : "n",
28604
28665
  attachDistance: Math.abs(y3 - centerY) < 0.2 || vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
28605
28666
  }, angleLabel, "\xB0"));
28606
28667
  };
28607
- const RightAngleArc = ({
28668
+
28669
+ /**
28670
+ * This is broken out into its own component so that it can be used for an early return
28671
+ * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
28672
+ */
28673
+ const RightAngleSquare = ({
28608
28674
  start: [x1, y1],
28609
28675
  vertex: [x2, y2],
28610
28676
  end: [x3, y3]
28611
- }) => /*#__PURE__*/React.createElement("g", {
28612
- style: {
28613
- transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28614
- }
28615
- }, /*#__PURE__*/React.createElement("path", {
28677
+ }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
28616
28678
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
28617
28679
  strokeWidth: 0.02,
28618
28680
  fill: "none"
28619
28681
  }));
28620
28682
  const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
28621
- const shouldDrawArcInside = (midPoint, vertex, range, polygonPoints) => {
28622
- const rangeIntersectionPoint = getRayIntersectionCoords(vertex, midPoint, range);
28623
- const lines = getLines(polygonPoints);
28624
- let lineIntersections = 0;
28625
- lines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersections++);
28626
-
28627
- // The intersection check will sometimes return false if it intersects with
28628
- // another vertex, so in the case we get 0 intersections, we check for points.
28629
- if (lineIntersections === 0) {
28630
- // find point in array
28631
- const midpointIndex = polygonPoints.findIndex(([x, y]) => x === midPoint[0] && y === midPoint[1]);
28632
- // get array sans point and adjacent points
28633
- const [prev, next] = [(midpointIndex - 1 + polygonPoints.length) % polygonPoints.length, (midpointIndex + 1) % polygonPoints.length];
28634
- const [[lineAx, lineAy], [lineBx, lineBy]] = [midPoint, rangeIntersectionPoint];
28635
- for (let i = 0; i < polygonPoints.length; i++) {
28636
- if ([prev, midpointIndex, next].includes(i)) {
28637
- continue;
28638
- }
28639
- const [x, y] = polygonPoints[i];
28640
- const intersectsPoint = (y - lineAy) * (lineBx - lineAx) === (lineBy - lineAy) * (x - lineAx);
28641
- if (intersectsPoint) {
28642
- return false;
28643
- }
28644
- }
28645
- return true;
28646
- }
28683
+
28684
+ /**
28685
+ * Determines if an angle is an inside (false) or outside (true) angle.
28686
+ * They way, we know to flip the `largeArc` and `sweepArc` flags.
28687
+ * Uses the priciple that a ray from a point inside a polygon will intersect
28688
+ * with an odd number of lines, while a ray from a point outside the polygon
28689
+ * will intersect with an even number of lines.
28690
+ * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
28691
+ */
28692
+ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
28693
+ // Create a ray from the midpoint (inside angle) to the edge of the range
28694
+ const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
28695
+ let lineIntersectionCount = 0;
28696
+ polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
28647
28697
 
28648
28698
  // If the number of intersections is even, the angle is inside the polygon
28649
- return isEven(lineIntersections);
28699
+ return !isEven(lineIntersectionCount);
28650
28700
  };
28651
- const getLines = points => points.map((point, i) => {
28652
- const next = points[(i + 1) % points.length];
28653
- return [point, next];
28654
- });
28655
28701
 
28656
28702
  // https://stackoverflow.com/a/24392281/7347484
28657
28703
  // The "intersects" function in geometry doesn't seem to work for this use case
@@ -28678,6 +28724,7 @@ const PolygonGraph = props => {
28678
28724
  coords,
28679
28725
  type,
28680
28726
  showAngles,
28727
+ showSides,
28681
28728
  range
28682
28729
  } = props.graphState;
28683
28730
  const points = coords != null ? coords : [[0, 0]];
@@ -28696,6 +28743,7 @@ const PolygonGraph = props => {
28696
28743
  constrain: p => p
28697
28744
  });
28698
28745
  const active = hovered || focused || dragging;
28746
+ const lines = getLines(points);
28699
28747
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polygon, {
28700
28748
  points: [...points],
28701
28749
  color: "var(--movable-line-stroke-color)",
@@ -28714,9 +28762,17 @@ const PolygonGraph = props => {
28714
28762
  endPoints: [pt1, pt2],
28715
28763
  active: active,
28716
28764
  range: range,
28717
- polygonPoints: points,
28765
+ polygonLines: lines,
28718
28766
  showAngles: !!showAngles
28719
28767
  });
28768
+ }), showSides && lines.map(([start, end], i) => {
28769
+ const [x, y] = vec.midpoint(start, end);
28770
+ const length = parseFloat(vec.dist(start, end).toFixed(1));
28771
+ return /*#__PURE__*/React.createElement(TextLabel, {
28772
+ key: "side-" + i,
28773
+ x: x,
28774
+ y: y
28775
+ }, !Number.isInteger(length) && "≈ ", length);
28720
28776
  }), /*#__PURE__*/React.createElement(Polygon, {
28721
28777
  points: [...points],
28722
28778
  color: "transparent",
@@ -28736,11 +28792,73 @@ const PolygonGraph = props => {
28736
28792
  }), points.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
28737
28793
  key: "point-" + i,
28738
28794
  point: point,
28739
- onMove: destination => dispatch(moveControlPoint(i, destination)),
28795
+ onMove: destination => dispatch(movePoint(i, destination)),
28740
28796
  "data-testid": type + i
28741
28797
  })));
28742
28798
  };
28743
28799
 
28800
+ function PointGraph(props) {
28801
+ const {
28802
+ dispatch
28803
+ } = props;
28804
+ return /*#__PURE__*/React.createElement(React.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
28805
+ key: i,
28806
+ point: point,
28807
+ onMove: destination => dispatch(movePoint(i, destination))
28808
+ })));
28809
+ }
28810
+
28811
+ const GraphStateContext = /*#__PURE__*/createContext({
28812
+ state: {},
28813
+ dispatch: () => {}
28814
+ });
28815
+ function useGraphState() {
28816
+ return React__default.useContext(GraphStateContext);
28817
+ }
28818
+
28819
+ function Arrow(props) {
28820
+ const {
28821
+ userTransform,
28822
+ viewTransform
28823
+ } = useTransformContext();
28824
+ const point = [props.x, props.y];
28825
+ const userTransformedPoint = vec.transform(point, userTransform);
28826
+ const viewTransformedPoint = vec.transform(userTransformedPoint, viewTransform);
28827
+ return /*#__PURE__*/React__default.createElement("g", {
28828
+ transform: `translate(${viewTransformedPoint[0]} ${viewTransformedPoint[1]}) rotate(${props.rotate}) scale(1.25)`
28829
+ }, /*#__PURE__*/React__default.createElement("g", {
28830
+ transform: "translate(-1)"
28831
+ }, /*#__PURE__*/React__default.createElement("path", {
28832
+ d: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
28833
+ fill: "none"
28834
+ })));
28835
+ }
28836
+ function AxisArrows() {
28837
+ const {
28838
+ state
28839
+ } = useGraphState();
28840
+ const range = state.range;
28841
+ const [xMin, xMax] = range[0];
28842
+ const [yMin, yMax] = range[1];
28843
+ return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Arrow, {
28844
+ x: xMax,
28845
+ y: 0,
28846
+ rotate: 0
28847
+ }), /*#__PURE__*/React__default.createElement(Arrow, {
28848
+ x: 0,
28849
+ y: yMin,
28850
+ rotate: 90
28851
+ }), /*#__PURE__*/React__default.createElement(Arrow, {
28852
+ x: xMin,
28853
+ y: 0,
28854
+ rotate: 180
28855
+ }), /*#__PURE__*/React__default.createElement(Arrow, {
28856
+ x: 0,
28857
+ y: yMax,
28858
+ rotate: 270
28859
+ }));
28860
+ }
28861
+
28744
28862
  const renderLineLabel = (n, [min, max]) => n !== -1 && n !== min && n !== max;
28745
28863
  const axisOptions = (props, index) => ({
28746
28864
  axis: props.markings === "graph",
@@ -28748,10 +28866,10 @@ const axisOptions = (props, index) => ({
28748
28866
  subdivisions: props.step[index] / props.gridStep[index],
28749
28867
  labels: n => renderLineLabel(n, props.range[index]) ? n : ""
28750
28868
  });
28751
- const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
28869
+ const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
28752
28870
  xAxis: axisOptions(props, 0),
28753
28871
  yAxis: axisOptions(props, 1)
28754
- });
28872
+ }), props.markings === "graph" && /*#__PURE__*/React.createElement(AxisArrows, null));
28755
28873
 
28756
28874
  // Displays a stylized open eye: lint warnings are visible
28757
28875
  const VisibleIcon = () => /*#__PURE__*/React.createElement("svg", {
@@ -29448,22 +29566,32 @@ const LegacyGrid = ({
29448
29566
  return null;
29449
29567
  };
29450
29568
 
29451
- /** Determine if coords is type CollinearTuple[] */
29452
- const isCollinearTuples = coords => Array.isArray(coords[0][0]);
29453
-
29454
- // Generic type makes returned state match input state
29455
29569
  function interactiveGraphReducer(state, action) {
29570
+ switch (action.type) {
29571
+ case MOVE_CONTROL_POINT:
29572
+ return doMoveControlPoint(state, action);
29573
+ case MOVE_LINE:
29574
+ return doMoveLine(state, action);
29575
+ case MOVE_ALL:
29576
+ return doMoveAll(state, action);
29577
+ case MOVE_POINT:
29578
+ return doMovePoint(state, action);
29579
+ default:
29580
+ throw new UnreachableCaseError(action);
29581
+ }
29582
+ }
29583
+ function doMoveControlPoint(state, action) {
29456
29584
  const {
29457
29585
  snapStep,
29458
29586
  range
29459
29587
  } = state;
29460
- if (!state.coords) {
29461
- throw new Error("Graph state must have been initialized with coords");
29462
- }
29463
- switch (action.type) {
29464
- case MOVE_CONTROL_POINT:
29588
+ switch (state.type) {
29589
+ case "segment":
29590
+ case "linear":
29591
+ case "linear-system":
29592
+ case "ray":
29465
29593
  {
29466
- const newCoords = action.itemIndex !== undefined && state.coords && isCollinearTuples(state.coords) ? updateAtIndex({
29594
+ const newCoords = updateAtIndex({
29467
29595
  array: state.coords,
29468
29596
  index: action.itemIndex,
29469
29597
  update: tuple => setAtIndex({
@@ -29478,23 +29606,9 @@ function interactiveGraphReducer(state, action) {
29478
29606
  })
29479
29607
  })
29480
29608
  })
29481
- }) : setAtIndex({
29482
- array: state.coords,
29483
- index: action.pointIndex,
29484
- newValue: snap({
29485
- snapStep,
29486
- point: bound$1({
29487
- snapStep,
29488
- range,
29489
- point: action.destination
29490
- })
29491
- })
29492
29609
  });
29493
-
29494
- // Cannot type narrow both conditions within function parameters,
29495
- // so this may seem redundant, but it's necessary for type safety.
29496
- const coordsToCheck = isCollinearTuples(newCoords) && action.itemIndex !== undefined ? newCoords[action.itemIndex] : newCoords;
29497
- if (!isCollinearTuples(coordsToCheck) && coordsOverlap(coordsToCheck)) {
29610
+ const coordsToCheck = newCoords[action.itemIndex];
29611
+ if (coordsOverlap(coordsToCheck)) {
29498
29612
  return state;
29499
29613
  }
29500
29614
  return _extends({}, state, {
@@ -29502,16 +29616,30 @@ function interactiveGraphReducer(state, action) {
29502
29616
  coords: newCoords
29503
29617
  });
29504
29618
  }
29505
- case MOVE_LINE:
29619
+ case "circle":
29620
+ throw new Error("FIXME implement circle reducer");
29621
+ case "point":
29622
+ case "polygon":
29623
+ throw new Error(`Don't use moveControlPoint for ${state.type} graphs. Use movePoint instead!`);
29624
+ default:
29625
+ throw new UnreachableCaseError(state);
29626
+ }
29627
+ }
29628
+ function doMoveLine(state, action) {
29629
+ const {
29630
+ snapStep,
29631
+ range
29632
+ } = state;
29633
+ switch (state.type) {
29634
+ case "segment":
29635
+ case "linear":
29636
+ case "linear-system":
29637
+ case "ray":
29506
29638
  {
29507
- var _state$coords;
29508
- if (!isCollinearTuples(state.coords)) {
29509
- throw new Error("Cannot call this action unless coords is array of CollinearTuple.");
29510
- }
29511
29639
  if (action.itemIndex === undefined) {
29512
29640
  throw new Error("Please provide index of line to move");
29513
29641
  }
29514
- const currentLine = (_state$coords = state.coords) == null ? void 0 : _state$coords[action.itemIndex];
29642
+ const currentLine = state.coords[action.itemIndex];
29515
29643
  if (!currentLine) {
29516
29644
  throw new Error("No line to move");
29517
29645
  }
@@ -29532,15 +29660,25 @@ function interactiveGraphReducer(state, action) {
29532
29660
  newValue: newLine
29533
29661
  });
29534
29662
  return _extends({}, state, {
29663
+ type: state.type,
29535
29664
  hasBeenInteractedWith: true,
29536
29665
  coords: newCoords
29537
29666
  });
29538
29667
  }
29539
- case MOVE_ALL:
29668
+ default:
29669
+ // The MoveLine action doesn't make sense for other graph types;
29670
+ // ignore it if it somehow happens
29671
+ return state;
29672
+ }
29673
+ }
29674
+ function doMoveAll(state, action) {
29675
+ const {
29676
+ snapStep,
29677
+ range
29678
+ } = state;
29679
+ switch (state.type) {
29680
+ case "polygon":
29540
29681
  {
29541
- if (isCollinearTuples(state.coords)) {
29542
- throw new Error("Cannot call this action unless coords is array of vectors.");
29543
- }
29544
29682
  const change = getChange(state.coords, action.delta, {
29545
29683
  snapStep,
29546
29684
  range
@@ -29555,7 +29693,33 @@ function interactiveGraphReducer(state, action) {
29555
29693
  });
29556
29694
  }
29557
29695
  default:
29558
- throw new UnreachableCaseError(action);
29696
+ // MoveAll is not supported for other state types; just ignore it.
29697
+ return state;
29698
+ }
29699
+ }
29700
+ function doMovePoint(state, action) {
29701
+ switch (state.type) {
29702
+ case "point":
29703
+ case "polygon":
29704
+ {
29705
+ return _extends({}, state, {
29706
+ hasBeenInteractedWith: true,
29707
+ coords: setAtIndex({
29708
+ array: state.coords,
29709
+ index: action.index,
29710
+ newValue: snap({
29711
+ snapStep: state.snapStep,
29712
+ point: bound$1({
29713
+ snapStep: state.snapStep,
29714
+ range: state.range,
29715
+ point: action.destination
29716
+ })
29717
+ })
29718
+ })
29719
+ });
29720
+ }
29721
+ default:
29722
+ throw new Error("The movePoint action is only for point and polygon graphs");
29559
29723
  }
29560
29724
  }
29561
29725
  const getChange = (coords, delta, constraintOpts) => {
@@ -29636,7 +29800,7 @@ function clamp(value, min, max) {
29636
29800
  const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && vector$1.equal(coord, c)));
29637
29801
  function updateAtIndex(args) {
29638
29802
  const {
29639
- array = [],
29803
+ array,
29640
29804
  index,
29641
29805
  update
29642
29806
  } = args;
@@ -29649,13 +29813,12 @@ function updateAtIndex(args) {
29649
29813
  }
29650
29814
  function setAtIndex(args) {
29651
29815
  const {
29652
- array = [],
29816
+ array,
29653
29817
  index,
29654
29818
  newValue
29655
29819
  } = args;
29656
29820
  const copy = [...array];
29657
29821
  copy[index] = newValue;
29658
- // restoring readonly to array
29659
29822
  return copy;
29660
29823
  }
29661
29824
 
@@ -29696,19 +29859,32 @@ function initializeGraphState(params) {
29696
29859
  })
29697
29860
  };
29698
29861
  case "polygon":
29699
- return _extends({
29862
+ return {
29863
+ type: "polygon",
29700
29864
  hasBeenInteractedWith: false,
29701
29865
  range,
29702
- snapStep
29703
- }, graph, {
29866
+ snapStep,
29867
+ showAngles: Boolean(graph.showAngles),
29868
+ showSides: Boolean(graph.showSides),
29704
29869
  coords: getPolygonCoords({
29705
29870
  graph,
29706
29871
  range,
29707
29872
  step
29708
29873
  })
29709
- });
29710
- case "angle":
29874
+ };
29711
29875
  case "point":
29876
+ return {
29877
+ type: graph.type,
29878
+ hasBeenInteractedWith: false,
29879
+ range,
29880
+ snapStep,
29881
+ coords: getDefaultPoints({
29882
+ graph,
29883
+ step,
29884
+ range
29885
+ })
29886
+ };
29887
+ case "angle":
29712
29888
  case "circle":
29713
29889
  case "sinusoid":
29714
29890
  case "quadratic":
@@ -29717,34 +29893,86 @@ function initializeGraphState(params) {
29717
29893
  throw new UnreachableCaseError(graph);
29718
29894
  }
29719
29895
  }
29896
+ const getDefaultPoints = ({
29897
+ graph,
29898
+ range,
29899
+ step
29900
+ }) => {
29901
+ var _graph$coords;
29902
+ const numPoints = graph.numPoints || 1;
29903
+ let coords = (_graph$coords = graph.coords) == null ? void 0 : _graph$coords.slice();
29904
+ if (coords) {
29905
+ return coords;
29906
+ }
29907
+ switch (numPoints) {
29908
+ case 1:
29909
+ // Back in the day, one point's coords were in graph.coord
29910
+ coords = [graph.coord || [0, 0]];
29911
+ break;
29912
+ case 2:
29913
+ coords = [[-5, 0], [5, 0]];
29914
+ break;
29915
+ case 3:
29916
+ coords = [[-5, 0], [0, 0], [5, 0]];
29917
+ break;
29918
+ case 4:
29919
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
29920
+ break;
29921
+ case 5:
29922
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
29923
+ break;
29924
+ case 6:
29925
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
29926
+ break;
29927
+ default:
29928
+ coords = [];
29929
+ break;
29930
+ }
29931
+ // Transform coords from their -10 to 10 space to 0 to 1
29932
+ // because of the old graph.coord, and also it's easier.
29933
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29934
+ return normalizePoints(range, step, newCoords);
29935
+ };
29720
29936
 
29721
29937
  // TS v4 doesn't narrow return types, while v5 does.
29722
29938
  // Instead of updating to v5, using generic type to relate input and output types.
29723
29939
  function getGradableGraph(state, initialGraph) {
29724
- var _state$coords;
29725
29940
  if (!state.hasBeenInteractedWith) {
29726
- return initialGraph;
29941
+ return {
29942
+ type: initialGraph.type
29943
+ };
29727
29944
  }
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 _extends({}, initialGraph, {
29733
- coords: state.coords
29734
- });
29735
- // coords: CollinearTuple
29736
- case state.type === "linear" && initialGraph.type === "linear":
29737
- case state.type === "ray" && initialGraph.type === "ray":
29738
- return _extends({}, initialGraph, {
29739
- coords: (_state$coords = state.coords) == null ? void 0 : _state$coords[0]
29740
- });
29741
- case state.type === "polygon" && initialGraph.type === "polygon":
29742
- return _extends({}, initialGraph, {
29743
- coords: state.coords
29744
- });
29745
- default:
29746
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29945
+ if (initialGraph.type === "linear-system" && state.type === "linear-system") {
29946
+ return _extends({}, initialGraph, {
29947
+ coords: state.coords
29948
+ });
29949
+ }
29950
+ if (state.type === "segment" && initialGraph.type === "segment") {
29951
+ return _extends({}, initialGraph, {
29952
+ coords: state.coords
29953
+ });
29954
+ }
29955
+ if (state.type === "linear" && initialGraph.type === "linear") {
29956
+ return _extends({}, initialGraph, {
29957
+ coords: state.coords[0]
29958
+ });
29959
+ }
29960
+ if (state.type === "ray" && initialGraph.type === "ray") {
29961
+ return _extends({}, initialGraph, {
29962
+ coords: state.coords[0]
29963
+ });
29964
+ }
29965
+ if (state.type === "polygon" && initialGraph.type === "polygon") {
29966
+ return _extends({}, initialGraph, {
29967
+ coords: state.coords
29968
+ });
29969
+ }
29970
+ if (state.type === "point" && initialGraph.type === "point") {
29971
+ return _extends({}, initialGraph, {
29972
+ coords: state.coords
29973
+ });
29747
29974
  }
29975
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29748
29976
  }
29749
29977
  const getDefaultSegments = ({
29750
29978
  graph,
@@ -29781,10 +30009,10 @@ const getLineCoords = ({
29781
30009
  range,
29782
30010
  step
29783
30011
  }) => {
29784
- var _graph$coords;
30012
+ var _graph$coords2;
29785
30013
  return (
29786
30014
  // Return two lines for a linear system, one for a ray or linear
29787
- ((_graph$coords = graph.coords) != null ? _graph$coords : graph.type === "linear-system") ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
30015
+ ((_graph$coords2 = graph.coords) != null ? _graph$coords2 : graph.type === "linear-system") ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
29788
30016
  );
29789
30017
  };
29790
30018
  const getPolygonCoords = ({
@@ -29792,7 +30020,8 @@ const getPolygonCoords = ({
29792
30020
  range,
29793
30021
  step
29794
30022
  }) => {
29795
- let coords = graph.coords;
30023
+ var _graph$coords3;
30024
+ let coords = (_graph$coords3 = graph.coords) == null ? void 0 : _graph$coords3.slice();
29796
30025
  if (coords) {
29797
30026
  return coords;
29798
30027
  }
@@ -29846,6 +30075,13 @@ const renderGraph = props => {
29846
30075
  graphState: state,
29847
30076
  dispatch: dispatch
29848
30077
  });
30078
+ case "point":
30079
+ return /*#__PURE__*/React.createElement(PointGraph, {
30080
+ graphState: state,
30081
+ dispatch: dispatch
30082
+ });
30083
+ case "circle":
30084
+ throw new Error("the circle graph type is not yet implemented");
29849
30085
  default:
29850
30086
  return new UnreachableCaseError(type);
29851
30087
  }
@@ -29853,10 +30089,24 @@ const renderGraph = props => {
29853
30089
  const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29854
30090
  const [width, height] = props.box;
29855
30091
  const [state, dispatch] = React.useReducer(interactiveGraphReducer, props, initializeGraphState);
30092
+ const prevState = useRef(state);
30093
+ useEffect(() => {
30094
+ if (prevState.current !== state) {
30095
+ props.onChange({
30096
+ graph: state
30097
+ });
30098
+ }
30099
+ prevState.current = state;
30100
+ }, [props, state]);
29856
30101
  React.useImperativeHandle(ref, () => ({
29857
30102
  getUserInput: () => getGradableGraph(state, props.graph)
29858
30103
  }));
29859
- return /*#__PURE__*/React.createElement(View, {
30104
+ return /*#__PURE__*/React.createElement(GraphStateContext.Provider, {
30105
+ value: {
30106
+ state,
30107
+ dispatch
30108
+ }
30109
+ }, /*#__PURE__*/React.createElement(View, {
29860
30110
  style: {
29861
30111
  width,
29862
30112
  height,
@@ -29882,12 +30132,12 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29882
30132
  zoom: false,
29883
30133
  width: width,
29884
30134
  height: height
29885
- }, /*#__PURE__*/React.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
30135
+ }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
29886
30136
  lockedFigures: props.lockedFigures
29887
30137
  }), renderGraph({
29888
30138
  state,
29889
30139
  dispatch
29890
- }))));
30140
+ })))));
29891
30141
  });
29892
30142
 
29893
30143
  const {
@@ -31024,6 +31274,7 @@ class LegacyInteractiveGraph extends React.Component {
31024
31274
  $(this.angle).on("move", () => {
31025
31275
  var _this$angle;
31026
31276
  this.onChange({
31277
+ // @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'.
31027
31278
  graph: _extends({}, graph, {
31028
31279
  coords: (_this$angle = this.angle) == null ? void 0 : _this$angle.getClockwiseCoords()
31029
31280
  })
@@ -31536,18 +31787,18 @@ class InteractiveGraph extends React.Component {
31536
31787
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
31537
31788
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
31538
31789
  }
31539
- static validate(
31540
- // TODO(kevinb): rename state to userInput
31541
- // state === userInput
31542
- state, rubric, component) {
31790
+ static validate(userInput, rubric, component) {
31543
31791
  // When nothing has moved, there will neither be coords nor the
31544
31792
  // circle's center/radius fields. When those fields are absent, skip
31545
31793
  // all these checks; just go mark the answer as empty.
31546
- // @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'.
31547
- const hasValue = !!(state.coords || state.center && state.radius);
31548
- if (state.type === rubric.correct.type && hasValue) {
31549
- if (state.type === "linear" && rubric.correct.type === "linear" && state.coords != null) {
31550
- const guess = state.coords;
31794
+ const hasValue = Boolean(
31795
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
31796
+ userInput.coords ||
31797
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
31798
+ userInput.center && userInput.radius);
31799
+ if (userInput.type === rubric.correct.type && hasValue) {
31800
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
31801
+ const guess = userInput.coords;
31551
31802
  const correct = rubric.correct.coords;
31552
31803
 
31553
31804
  // If both of the guess points are on the correct line, it's
@@ -31564,8 +31815,8 @@ class InteractiveGraph extends React.Component {
31564
31815
  message: null
31565
31816
  };
31566
31817
  }
31567
- } else if (state.type === "linear-system" && rubric.correct.type === "linear-system" && state.coords != null) {
31568
- const guess = state.coords;
31818
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
31819
+ const guess = userInput.coords;
31569
31820
  const correct = rubric.correct.coords;
31570
31821
  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])) {
31571
31822
  return {
@@ -31575,9 +31826,9 @@ class InteractiveGraph extends React.Component {
31575
31826
  message: null
31576
31827
  };
31577
31828
  }
31578
- } else if (state.type === "quadratic" && rubric.correct.type === "quadratic" && state.coords != null) {
31829
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
31579
31830
  // If the parabola coefficients match, it's correct.
31580
- const guessCoeffs = this.getQuadraticCoefficients(state.coords);
31831
+ const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
31581
31832
  const correctCoeffs = this.getQuadraticCoefficients(
31582
31833
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31583
31834
  rubric.correct.coords);
@@ -31589,8 +31840,8 @@ class InteractiveGraph extends React.Component {
31589
31840
  message: null
31590
31841
  };
31591
31842
  }
31592
- } else if (state.type === "sinusoid" && rubric.correct.type === "sinusoid" && state.coords != null) {
31593
- const guessCoeffs = this.getSinusoidCoefficients(state.coords);
31843
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
31844
+ const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
31594
31845
  const correctCoeffs = this.getSinusoidCoefficients(
31595
31846
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31596
31847
  rubric.correct.coords);
@@ -31605,8 +31856,8 @@ class InteractiveGraph extends React.Component {
31605
31856
  message: null
31606
31857
  };
31607
31858
  }
31608
- } else if (state.type === "circle" && rubric.correct.type === "circle") {
31609
- if (deepEq$1(state.center, rubric.correct.center) && eq(state.radius, rubric.correct.radius)) {
31859
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
31860
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
31610
31861
  return {
31611
31862
  type: "points",
31612
31863
  earned: 1,
@@ -31614,10 +31865,9 @@ class InteractiveGraph extends React.Component {
31614
31865
  message: null
31615
31866
  };
31616
31867
  }
31617
- } else if (state.type === "point" && rubric.correct.type === "point" && state.coords != null) {
31618
- var _state$coords;
31868
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
31619
31869
  let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
31620
- const guess = (_state$coords = state.coords) == null ? void 0 : _state$coords.slice();
31870
+ const guess = userInput.coords.slice();
31621
31871
  correct = correct.slice();
31622
31872
  // Everything's already rounded so we shouldn't need to do an
31623
31873
  // eq() comparison but _.isEqual(0, -0) is false, so we'll use
@@ -31634,9 +31884,9 @@ class InteractiveGraph extends React.Component {
31634
31884
  message: null
31635
31885
  };
31636
31886
  }
31637
- } else if (state.type === "polygon" && rubric.correct.type === "polygon" && state.coords != null) {
31638
- var _state$coords2, _rubric$correct$coord;
31639
- const guess = (_state$coords2 = state.coords) == null ? void 0 : _state$coords2.slice();
31887
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
31888
+ var _userInput$coords, _rubric$correct$coord;
31889
+ const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
31640
31890
  // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
31641
31891
  const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
31642
31892
  let match;
@@ -31660,9 +31910,9 @@ class InteractiveGraph extends React.Component {
31660
31910
  message: null
31661
31911
  };
31662
31912
  }
31663
- } else if (state.type === "segment" && rubric.correct.type === "segment" && state.coords != null) {
31913
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
31664
31914
  var _rubric$correct$coord2;
31665
- let guess = state.coords.slice();
31915
+ let guess = userInput.coords.slice();
31666
31916
  let correct = (_rubric$correct$coord2 = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord2.slice();
31667
31917
  guess = _.invoke(guess, "sort").sort();
31668
31918
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
@@ -31675,8 +31925,8 @@ class InteractiveGraph extends React.Component {
31675
31925
  message: null
31676
31926
  };
31677
31927
  }
31678
- } else if (state.type === "ray" && rubric.correct.type === "ray" && state.coords != null) {
31679
- const guess = state.coords;
31928
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
31929
+ const guess = userInput.coords;
31680
31930
  const correct = rubric.correct.coords;
31681
31931
  if (
31682
31932
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
@@ -31690,8 +31940,8 @@ class InteractiveGraph extends React.Component {
31690
31940
  message: null
31691
31941
  };
31692
31942
  }
31693
- } else if (state.type === "angle" && rubric.correct.type === "angle") {
31694
- const guess = state.coords;
31943
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
31944
+ const guess = userInput.coords;
31695
31945
  const correct = rubric.correct.coords;
31696
31946
  let match;
31697
31947
  if (rubric.correct.match === "congruent") {
@@ -31730,7 +31980,7 @@ class InteractiveGraph extends React.Component {
31730
31980
 
31731
31981
  // The input wasn't correct, so check if it's a blank input or if it's
31732
31982
  // actually just wrong
31733
- if (!hasValue || _.isEqual(state, rubric.graph)) {
31983
+ if (!hasValue || _.isEqual(userInput, rubric.graph)) {
31734
31984
  // We're where we started.
31735
31985
  return {
31736
31986
  type: "invalid",
@@ -41931,7 +42181,7 @@ const init = function init(options) {
41931
42181
 
41932
42182
  // This file is processed by a Rollup plugin (replace) to inject the production
41933
42183
  const libName = "@khanacademy/perseus";
41934
- const libVersion = "20.8.1";
42184
+ const libVersion = "21.1.0";
41935
42185
  addLibraryVersionToPerseusDebug(libName, libVersion);
41936
42186
 
41937
42187
  const apiVersion = {
@@ -44183,6 +44433,140 @@ const registerAllWidgetsForTesting = () => {
44183
44433
  replaceDeprecatedWidgets();
44184
44434
  };
44185
44435
 
44436
+ // These are all the current widgets that require user-input and are supported for automatic scoring
44437
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44438
+
44439
+ /**
44440
+ * Regex for widget placeholders in a string.
44441
+ *
44442
+ * First capture group is the widget ID (ex. 'radio 1')
44443
+ * Second capture group is the widget type (ex. "radio)
44444
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44445
+ */
44446
+ function getWidgetRegex() {
44447
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
44448
+ }
44449
+
44450
+ /**
44451
+ * Add a widget placeholder using the widget ID.
44452
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
44453
+ *
44454
+ * @param {string} id
44455
+ * @returns {string}
44456
+ */
44457
+ function addWidget(id) {
44458
+ return `[[☃ ${id}]]`;
44459
+ }
44460
+
44461
+ /**
44462
+ * Get a widget type by a widget's ID
44463
+ * (please don't derive type from ID)
44464
+ *
44465
+ * @param {string} widgetId the ID of the widget
44466
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44467
+ * @returns {string} the widget type (ie "radio")
44468
+ */
44469
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44470
+ var _widget$type;
44471
+ const widget = widgetMap[WidgetId];
44472
+ return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
44473
+ }
44474
+
44475
+ /**
44476
+ * Does the content have a specific type of widget?
44477
+ *
44478
+ * @param {string} type the type of the widget in question (ie "radio")
44479
+ * @param {string} content the string to search through
44480
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44481
+ * @returns {boolean} if the content includes the widget type
44482
+ */
44483
+ function contentHasWidgetType(type, content, widgetMap) {
44484
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
44485
+ }
44486
+
44487
+ /**
44488
+ * Extract all widget IDs, which includes the widget type and instance number.
44489
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44490
+ *
44491
+ * Content should contain Perseus widget placeholders,
44492
+ * which look like: '[[☃ radio 1]]'.
44493
+ *
44494
+ * @param {string} content
44495
+ * @returns {ReadonlyArray<string>} widgetIds
44496
+ */
44497
+ function getWidgetIdsFromContent(content) {
44498
+ const widgets = [];
44499
+ const localWidgetRegex = getWidgetRegex();
44500
+ let match = localWidgetRegex.exec(content);
44501
+ while (match !== null) {
44502
+ widgets.push(match[1]);
44503
+ match = localWidgetRegex.exec(content);
44504
+ }
44505
+ return widgets;
44506
+ }
44507
+
44508
+ /**
44509
+ * Get a list of widget IDs from content,
44510
+ * but only for specific widget types
44511
+ *
44512
+ * @param {string} type the type of widget (ie "radio")
44513
+ * @param {string} content the string to parse
44514
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44515
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
44516
+ */
44517
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
44518
+ const rv = [];
44519
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
44520
+ widgetIdsInContent.forEach(widgetId => {
44521
+ const widget = widgetMap[widgetId];
44522
+ if ((widget == null ? void 0 : widget.type) === type) {
44523
+ rv.push(widgetId);
44524
+ }
44525
+ });
44526
+ return rv;
44527
+ }
44528
+
44529
+ /**
44530
+ * Pull the widget map out of ItemData
44531
+ *
44532
+ * @param {PerseusItem} PerseusItem containing a widgetMap
44533
+ * @returns {WidgetMap} the widget map in the PerseusItem
44534
+ */
44535
+ function getWidgetsMapFromItemData(itemData) {
44536
+ return itemData.question.widgets;
44537
+ }
44538
+
44539
+ /**
44540
+ * Get the widget information from a WidgetMap
44541
+ *
44542
+ * @param {string} widgetId the ID of the widget
44543
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
44544
+ */
44545
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
44546
+ var _widgetMap$widgetId;
44547
+ return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
44548
+ }
44549
+
44550
+ /**
44551
+ * Get select widgets from a widget map.
44552
+ * Useful for multi-items that needs to split
44553
+ * one PerseusItem into several
44554
+ *
44555
+ * @param {ReadonlyArray<string>} widgetIds to extract
44556
+ * @param {PerseusWidgetsMap} widgetMap to extract from
44557
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
44558
+ */
44559
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
44560
+ const widgets = {};
44561
+ widgetIds.forEach(widgetId => {
44562
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
44563
+ if (widget) {
44564
+ widgets[widgetId] = widget;
44565
+ }
44566
+ });
44567
+ return widgets;
44568
+ }
44569
+
44186
44570
  /**
44187
44571
  * This function extracts the answers from the widgets.
44188
44572
  *
@@ -44307,7 +44691,7 @@ function getAnswersFromWidgets(widgets) {
44307
44691
  // E.g. 'item 1\nitem 2\nitem 3'
44308
44692
  const orderer = widget;
44309
44693
  if ((_orderer$options = orderer.options) != null && _orderer$options.correctOptions) {
44310
- answers.push(orderer.options.correctOptions.map(option => option.content).join("\n"));
44694
+ answers.push(joinOptionContents(orderer.options.correctOptions));
44311
44695
  }
44312
44696
  break;
44313
44697
  case "sorter":
@@ -44366,6 +44750,28 @@ function getAnswersFromWidgets(widgets) {
44366
44750
  return answers;
44367
44751
  }
44368
44752
 
44753
+ /**
44754
+ * Join the content of the options together.
44755
+ * @param {Array<{content: string}>} options
44756
+ * @returns {string}
44757
+ */
44758
+ const joinOptionContents = options => options.map(({
44759
+ content
44760
+ }) => content).join("\n");
44761
+
44762
+ /**
44763
+ * Convert an index to an option letter.
44764
+ * @param {number} index
44765
+ * @returns {string}
44766
+ * @example
44767
+ * toOptionLetter(0) // 'A'
44768
+ * toOptionLetter(1) // 'B'
44769
+ * toOptionLetter(25) // 'Z'
44770
+ * // Once the index goes past 25, it will start returning special characters
44771
+ * toOptionLetter(26) // '['
44772
+ */
44773
+ const toOptionLetter = index => String.fromCharCode("A".charCodeAt(0) + index);
44774
+
44369
44775
  /**
44370
44776
  * Inject a string equivalent of the widgets into the content.
44371
44777
  *
@@ -44376,7 +44782,7 @@ function getAnswersFromWidgets(widgets) {
44376
44782
  * @param {PerseusRenderer["widgets"]} widgets
44377
44783
  * @returns
44378
44784
  */
44379
- function injectWidgets(content, widgets) {
44785
+ function injectWidgets(content, widgets, widgetProps) {
44380
44786
  // The types for taskProgress.itemData are not well defined,
44381
44787
  // so there is a chance that widgets or content could be undefined.
44382
44788
  // In that scenario, simply exit early and return the content as is.
@@ -44396,12 +44802,20 @@ function injectWidgets(content, widgets) {
44396
44802
  switch (widget.type) {
44397
44803
  case "radio":
44398
44804
  // Replace radio with the radio options
44399
- // '[[☃ Radio 1]]' -> 'option 1\noption 2\noption 3'
44805
+ // '[[☃ Radio 1]]' -> 'choice 1\nchoice 2\nchoice 3'
44806
+ // or if the current widget state is available with proper order,
44807
+ // '[[☃ Radio 1]]' ->
44808
+ // 'Option A: choice 1\nOption B: choice 2\nOption C: choice 3'
44400
44809
  const radio = widget;
44810
+ const radioProps = widgetProps == null ? void 0 : widgetProps[widgetID];
44401
44811
  if ((_radio$options = radio.options) != null && (_radio$options = _radio$options.choices) != null && _radio$options.length) {
44402
44812
  var _radio$options2;
44403
- let radioContext = radio.options.choices.map(choice => choice.content).join("\n");
44404
- if ((_radio$options2 = radio.options) != null && _radio$options2.randomize) {
44813
+ let radioContext = joinOptionContents(radioProps ? radioProps.choices.map(({
44814
+ content
44815
+ }, i) => ({
44816
+ content: `Option ${toOptionLetter(i)}: ${content}`
44817
+ })) : radio.options.choices);
44818
+ if (!radioProps && (_radio$options2 = radio.options) != null && _radio$options2.randomize) {
44405
44819
  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";
44406
44820
  }
44407
44821
  context = context.replace(`[[☃ ${widgetID}]]`, radioContext);
@@ -44510,7 +44924,7 @@ function injectWidgets(content, widgets) {
44510
44924
  // '[[☃ Orderer 1]]' -> 'option 1\noption 2\noption 3'
44511
44925
  const orderer = widget;
44512
44926
  if ((_orderer$options2 = orderer.options) != null && _orderer$options2.options) {
44513
- context = context.replace(`[[☃ ${widgetID}]]`, orderer.options.options.map(option => option.content).join("\n"));
44927
+ context = context.replace(`[[☃ ${widgetID}]]`, joinOptionContents(orderer.options.options));
44514
44928
  }
44515
44929
  break;
44516
44930
  case "sorter":
@@ -44593,18 +45007,15 @@ const INDIVIDUAL_ANSWER_WIDGETS = ["interactive-graph", "categorizer", "grapher"
44593
45007
 
44594
45008
  /* Widgets that are supported for automatic scoring */
44595
45009
  const SUPPORTED_WIDGETS = ["radio", "numeric-input", "input-number", "expression", ...INDIVIDUAL_ANSWER_WIDGETS];
44596
- const getWidgetTypeFromWidgetKey = widgetKey => {
44597
- return widgetKey.split(" ")[0];
44598
- };
44599
45010
 
44600
45011
  /* Verify if the perseus item has supported widgets for automatic scoring */
44601
- const isWrongAnswerSupported = widgetKeys => {
44602
- return widgetKeys.length !== 0 && widgetKeys.every(widgetKey => SUPPORTED_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey)));
45012
+ const isWrongAnswerSupported = (widgetIds, widgetMap) => {
45013
+ return widgetIds.length !== 0 && widgetIds.every(widgetId => SUPPORTED_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap)));
44603
45014
  };
44604
45015
 
44605
- /* Verify if the widget key has an individual answer for the coach report view */
44606
- const shouldHaveIndividualAnswer = widgetKey => {
44607
- return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey));
45016
+ /* Verify if the widget ID has an individual answer for the coach report view */
45017
+ const shouldHaveIndividualAnswer = (widgetId, widgetMap) => {
45018
+ return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap));
44608
45019
  };
44609
45020
 
44610
45021
  /* Returns the answer userInput submission for currently supported widgets */
@@ -44623,29 +45034,30 @@ const getAnswerFromUserInput = (widgetType, userInput) => {
44623
45034
  return userInput;
44624
45035
  };
44625
45036
 
44626
- /* Returns the correct answer for a given widget key and Perseus Item */
45037
+ /* Returns the correct answer for a given widget ID and Perseus Item */
44627
45038
  // TODO (LEMS-1835): We should fix the resonse type from getWidget to be specific.
44628
45039
  // TODO (LEMS-1836): We should also consider adding the getOneCorrectAnswerFromRubric method to all widgets.
44629
- const getCorrectAnswerForWidgetKey = (widgetKey, itemData) => {
44630
- const rubric = itemData.question.widgets[widgetKey].options;
44631
- const widgetType = getWidgetTypeFromWidgetKey(widgetKey);
45040
+ const getCorrectAnswerForWidgetId = (widgetId, itemData) => {
45041
+ const rubric = itemData.question.widgets[widgetId].options;
45042
+ const widgetMap = getWidgetsMapFromItemData(itemData);
45043
+ const widgetType = getWidgetTypeByWidgetId(widgetId, widgetMap);
44632
45044
  const widget = getWidget(widgetType);
44633
45045
 
44634
45046
  // @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType<any>'.
44635
45047
  return widget == null || widget.getOneCorrectAnswerFromRubric == null ? void 0 : widget.getOneCorrectAnswerFromRubric(rubric);
44636
45048
  };
44637
45049
 
44638
- /* Verify if the widget key exists in the content string of the Perseus Item */
44639
- const isWidgetKeyInContent = (perseusItem, widgetKey) => {
44640
- return perseusItem.question.content.indexOf(widgetKey) !== -1;
45050
+ /* Verify if the widget ID exists in the content string of the Perseus Item */
45051
+ const isWidgetIdInContent = (perseusItem, widgetId) => {
45052
+ return perseusItem.question.content.indexOf(widgetId) !== -1;
44641
45053
  };
44642
45054
 
44643
- /* Return an array of all the widget keys that exist in the content string of a Perseus Item */
44644
- const getValidWidgetKeys = perseusItem => {
45055
+ /* Return an array of all the widget IDs that exist in the content string of a Perseus Item */
45056
+ const getValidWidgetIds = perseusItem => {
44645
45057
  const {
44646
45058
  widgets
44647
45059
  } = perseusItem.question;
44648
- return keys(widgets).filter(key => isWidgetKeyInContent(perseusItem, key));
45060
+ return keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
44649
45061
  };
44650
45062
 
44651
45063
  /**
@@ -44693,12 +45105,113 @@ const genericPerseusItemData = {
44693
45105
  * uses a basic Perseus object and updates it with custom values as needed.
44694
45106
  *
44695
45107
  * @param {Partial<PerseusItem>} customFields
44696
- * @returns PerseusItem
45108
+ * @returns {PerseusItem}
44697
45109
  */
44698
45110
  function generateTestPerseusItem(customFields = {}) {
44699
45111
  return _extends({}, genericPerseusItemData, customFields);
44700
45112
  }
44701
45113
 
45114
+ /**
45115
+ * Creates an object with the bare amount
45116
+ * of data to be a properly typed RadioWidget
45117
+ *
45118
+ * @returns {RadioWidget} skeleton RadioWidget for testing
45119
+ */
45120
+ function generateTestRadioWidget() {
45121
+ return {
45122
+ type: "radio",
45123
+ options: {
45124
+ choices: []
45125
+ }
45126
+ };
45127
+ }
45128
+
45129
+ /**
45130
+ * Creates an object with the bare amount
45131
+ * of data to be a properly typed InteractiveGraphWidget
45132
+ *
45133
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
45134
+ */
45135
+ function generateTestInteractiveGraphWidget() {
45136
+ return {
45137
+ type: "interactive-graph",
45138
+ options: {
45139
+ step: [1, 1],
45140
+ gridStep: [1, 1],
45141
+ snapStep: [1, 1],
45142
+ markings: "graph",
45143
+ labels: ["x", "y"],
45144
+ showProtractor: false,
45145
+ showRuler: false,
45146
+ rulerLabel: "mm",
45147
+ rulerTicks: 2,
45148
+ range: [[-10, 10], [-10, 10]],
45149
+ graph: {
45150
+ type: "angle"
45151
+ },
45152
+ correct: {
45153
+ type: "angle"
45154
+ }
45155
+ }
45156
+ };
45157
+ }
45158
+
45159
+ /**
45160
+ * Creates an object with the bare amount
45161
+ * of data to be a properly typed InteractiveGraphWidget
45162
+ *
45163
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
45164
+ */
45165
+ function generateTestCategorizerWidget() {
45166
+ return {
45167
+ type: "categorizer",
45168
+ options: {
45169
+ items: [],
45170
+ categories: [],
45171
+ randomizeItems: false,
45172
+ static: false,
45173
+ values: []
45174
+ }
45175
+ };
45176
+ }
45177
+
45178
+ /**
45179
+ * Creates an object with the bare amount
45180
+ * of data to be a properly typed ExpressionWidget
45181
+ *
45182
+ * @returns {ExpressionWidget} skeleton ExpressionWidget for testing
45183
+ */
45184
+ function generateTestExpressionWidget() {
45185
+ return {
45186
+ type: "expression",
45187
+ options: {
45188
+ answerForms: [],
45189
+ buttonSets: ["basic"],
45190
+ functions: [],
45191
+ times: false
45192
+ }
45193
+ };
45194
+ }
45195
+
45196
+ /**
45197
+ * Creates an object with the bare amount
45198
+ * of data to be a properly typed NumericInputWidget
45199
+ *
45200
+ * @returns {NumericInputWidget} skeleton NumericInputWidget for testing
45201
+ */
45202
+ function generateTestNumericInputWidget() {
45203
+ return {
45204
+ type: "numeric-input",
45205
+ options: {
45206
+ answers: [],
45207
+ labelText: "",
45208
+ size: "normal",
45209
+ coefficient: false,
45210
+ static: false
45211
+ }
45212
+ };
45213
+ }
45214
+
44702
45215
  /**
44703
45216
  * Some helpers for CEDAR logging. CEDAR convention dictates
44704
45217
  * that enum values should be upper case. Ex: for the
@@ -44753,162 +45266,6 @@ function convertWidgetNameToEnum(name) {
44753
45266
  }
44754
45267
  return widgetEnum;
44755
45268
  }
44756
- function getWidgetTypeFromWidgetId(widgetId) {
44757
- const [widgetType, widgetNumber] = widgetId.split(" ");
44758
- if (!widgetNumber) {
44759
- throw new Error(`Incorrect format for widget ID: ${widgetId}`);
44760
- }
44761
- if (!widgetNameToEnum[widgetType]) {
44762
- throw new Error(`Unknown widget type (${widgetType}) in widget ID: ${widgetId}`);
44763
- }
44764
- return widgetType;
44765
- }
44766
-
44767
- let WidgetType = /*#__PURE__*/function (WidgetType) {
44768
- WidgetType["Categorizer"] = "categorizer";
44769
- WidgetType["CSProgram"] = "cs-program";
44770
- WidgetType["Definition"] = "definition";
44771
- WidgetType["Dropdown"] = "dropdown";
44772
- WidgetType["Explanation"] = "explanation";
44773
- WidgetType["Expression"] = "expression";
44774
- WidgetType["GradedGroup"] = "graded-group";
44775
- WidgetType["GradedGroupAnswerBar"] = "graded-group-answer-bar";
44776
- WidgetType["GradedGroupSet"] = "graded-group-set";
44777
- WidgetType["Grapher"] = "grapher";
44778
- WidgetType["Group"] = "group";
44779
- WidgetType["Iframe"] = "iframe";
44780
- WidgetType["Image"] = "image";
44781
- WidgetType["InputNumber"] = "input-number";
44782
- WidgetType["Interaction"] = "interaction";
44783
- WidgetType["InteractiveGraph"] = "interactive-graph";
44784
- WidgetType["InteractiveNumberLine"] = "interactive-number-line";
44785
- WidgetType["LabelImage"] = "label-image";
44786
- WidgetType["Matcher"] = "matcher";
44787
- WidgetType["Matrix"] = "matrix";
44788
- WidgetType["Measurer"] = "measurer";
44789
- WidgetType["Molecule"] = "molecule";
44790
- WidgetType["NumberLine"] = "number-line";
44791
- WidgetType["NumericInput"] = "numeric-input";
44792
- WidgetType["Orderer"] = "orderer";
44793
- WidgetType["Passage"] = "passage";
44794
- WidgetType["PassageRef"] = "passage-ref";
44795
- WidgetType["PassageRefTarget"] = "passage-ref-target";
44796
- WidgetType["Plotter"] = "plotter";
44797
- WidgetType["Protractor"] = "protractor";
44798
- WidgetType["PythonProgram"] = "python-program";
44799
- WidgetType["Radio"] = "radio";
44800
- WidgetType["Sorter"] = "sorter";
44801
- WidgetType["Table"] = "table";
44802
- WidgetType["Unit"] = "unit";
44803
- WidgetType["Video"] = "video";
44804
- WidgetType["VideoTranscriptLink"] = "video-transcript-link";
44805
- return WidgetType;
44806
- }({});
44807
- // These are all the current widgets that require user-input and are supported for automatic scoring
44808
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44809
-
44810
- /**
44811
- * Regex for widget placeholders in a string.
44812
- *
44813
- * First capture group is the widget ID (ex. 'radio 1')
44814
- * Second capture group is the widget type (ex. "radio)
44815
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44816
- */
44817
- function getWidgetRegex() {
44818
- return /\[\[☃ (([a-z-]+) \d+)\]\]/g;
44819
- }
44820
-
44821
- /**
44822
- * Add a widget placeholder using the widget ID.
44823
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
44824
- *
44825
- * @param {string} id
44826
- * @returns {string}
44827
- */
44828
- function addWidget(id) {
44829
- return `[[☃ ${id}]]`;
44830
- }
44831
-
44832
- /**
44833
- * Extract all widget IDs, which includes the widget type and instance number.
44834
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44835
- *
44836
- * Content should contain Perseus widget placeholders,
44837
- * which look like: '[[☃ radio 1]]'.
44838
- *
44839
- * @param {string} content
44840
- * @returns {Array<WidgetId>} widgets
44841
- */
44842
- function getAllWidgetIds(content) {
44843
- const widgets = [];
44844
- const localWidgetRegex = getWidgetRegex();
44845
- let match = localWidgetRegex.exec(content);
44846
- while (match !== null) {
44847
- widgets.push(match[1]);
44848
- match = localWidgetRegex.exec(content);
44849
- }
44850
- return widgets;
44851
- }
44852
-
44853
- /**
44854
- * Check if a specific widget is an actual widget type using its name.
44855
- * The widget name should include a widget type, without an instance number,
44856
- * as a string. ex. 'radio'
44857
- *
44858
- * @param {string} widgetName
44859
- * @returns {boolean}
44860
- */
44861
- function isWidgetType(widgetName) {
44862
- return Object.values(WidgetType).includes(widgetName);
44863
- }
44864
-
44865
- /**
44866
- * Extract all widget types from a Perseus JSON content string.
44867
- * This does not include the instance number and prevents duplicates.
44868
- * example output: ['radio', 'categorizer']
44869
- *
44870
- * @param {string} content
44871
- * @returns {Array<WidgetId>}
44872
- */
44873
- function getAllWidgetTypes(content) {
44874
- const widgetTypes = [];
44875
- const localWidgetRegex = getWidgetRegex();
44876
- let match = localWidgetRegex.exec(content);
44877
- while (match !== null) {
44878
- const matchType = match[2];
44879
- if (!widgetTypes.includes(matchType) && isWidgetType(matchType)) {
44880
- widgetTypes.push(matchType);
44881
- }
44882
- match = localWidgetRegex.exec(content);
44883
- }
44884
- return widgetTypes;
44885
- }
44886
-
44887
- /**
44888
- * Check if a specific widget is a question widget type using its ID.
44889
- * The widget ID includes the widget type and the instance number.
44890
- *
44891
- * @param {WidgetId} widgetId
44892
- * @returns {boolean}
44893
- */
44894
- function isQuestionWidgetType(widgetId) {
44895
- return QUESTION_WIDGETS.includes(widgetId.split(" ")[0]);
44896
- }
44897
-
44898
- /**
44899
- * Extract the widget IDs of all widgets considered question widgets. Widget IDs
44900
- * include widget type and instance number.
44901
- * example output: ['radio 1', 'categorizer 1']
44902
- *
44903
- * Content should contain Perseus widget placeholders,
44904
- * which look like: '[[☃ radio 1]]'.
44905
- *
44906
- * @param {string} content
44907
- * @returns Array<WidgetId>
44908
- */
44909
- function getQuestionWidgetIds(content) {
44910
- return getAllWidgetIds(content).filter(isQuestionWidgetType);
44911
- }
44912
45269
 
44913
45270
  const EditorJsonify = {
44914
45271
  serialize: function () {
@@ -44918,5 +45275,5 @@ const EditorJsonify = {
44918
45275
  }
44919
45276
  };
44920
45277
 
44921
- export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, WidgetType, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, convertWidgetNameToEnum, displaySigFigs, generateTestPerseusItem, genericPerseusItemData, getAllWidgetIds, getAllWidgetTypes, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetKey, getQuestionWidgetIds, getValidWidgetKeys, getWidgetTypeFromWidgetId, getWidgetTypeFromWidgetKey, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetKeyInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, violatingWidgets, allWidgets as widgets };
45278
+ export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, violatingWidgets, allWidgets as widgets };
44922
45279
  //# sourceMappingURL=index.js.map