@khanacademy/perseus 20.6.0 → 20.8.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.
package/dist/index.js CHANGED
@@ -18894,7 +18894,7 @@ const isAccessible = widgetInfo => {
18894
18894
  }
18895
18895
  return !!accessible;
18896
18896
  };
18897
- const getAllWidgetTypes = () => {
18897
+ const getAllWidgetTypes$1 = () => {
18898
18898
  return ___default["default"].keys(widgets);
18899
18899
  };
18900
18900
  const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
@@ -19166,7 +19166,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19166
19166
  getVersionVector: getVersionVector,
19167
19167
  getPublicWidgets: getPublicWidgets,
19168
19168
  isAccessible: isAccessible,
19169
- getAllWidgetTypes: getAllWidgetTypes,
19169
+ getAllWidgetTypes: getAllWidgetTypes$1,
19170
19170
  upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
19171
19171
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
19172
19172
  traverseChildWidgets: traverseChildWidgets$3,
@@ -28320,22 +28320,55 @@ const GraphLockedLayer = props => {
28320
28320
  };
28321
28321
 
28322
28322
  const MOVE_CONTROL_POINT = "move-control-point";
28323
- function moveControlPoint(objectIndex, pointIndex, destination) {
28323
+ function moveControlPoint(pointIndex, destination, itemIndex) {
28324
28324
  return {
28325
28325
  type: MOVE_CONTROL_POINT,
28326
- objectIndex,
28326
+ itemIndex,
28327
28327
  pointIndex,
28328
28328
  destination
28329
28329
  };
28330
28330
  }
28331
+ const MOVE_ALL = "move-all";
28331
28332
  const MOVE_LINE = "move-line";
28332
- function moveSegment(lineIndex, delta) {
28333
+ /** This action assumes the state.coords holds an array of collinear tuples that define lines */
28334
+ function moveLine(itemIndex, delta) {
28333
28335
  return {
28334
28336
  type: MOVE_LINE,
28335
- lineIndex,
28337
+ itemIndex,
28336
28338
  delta
28337
28339
  };
28338
28340
  }
28341
+ /** This action assumes a flat array of vectors */
28342
+ const moveAll = delta => ({
28343
+ type: MOVE_ALL,
28344
+ delta
28345
+ });
28346
+
28347
+ /**
28348
+ * 44 is touch best practice and AAA compliant for WCAG
28349
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
28350
+ */
28351
+ const TARGET_SIZE = 44;
28352
+
28353
+ // same as pointsFromNormalized in interactive-graph.tsx
28354
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
28355
+ // maps over [x, y]
28356
+ coords.map((coord, i) => {
28357
+ const axisRange = range[i];
28358
+ if (noSnap) {
28359
+ return axisRange[0] + (axisRange[1] - axisRange[0]) * coord;
28360
+ }
28361
+ const axisStep = step[i];
28362
+ const nSteps = Math.floor((axisRange[1] - axisRange[0]) / axisStep);
28363
+ const tick = Math.round(coord * nSteps);
28364
+ return axisRange[0] + axisStep * tick;
28365
+ }));
28366
+
28367
+ // same as normalizeCoords in interactive-graph.tsx
28368
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
28369
+ const extent = ranges[i][1] - ranges[i][0];
28370
+ return (coord + ranges[i][1]) / extent;
28371
+ }));
28339
28372
 
28340
28373
  const defaultStroke = "var(--movable-line-stroke-color)";
28341
28374
  const MovableLine = props => {
@@ -28383,7 +28416,7 @@ const MovableLine = props => {
28383
28416
  end: endPtPx,
28384
28417
  style: {
28385
28418
  stroke: "transparent",
28386
- strokeWidth: 44
28419
+ strokeWidth: TARGET_SIZE
28387
28420
  }
28388
28421
  }), /*#__PURE__*/React__namespace.createElement(SVGLine, {
28389
28422
  start: startPtPx,
@@ -28416,7 +28449,7 @@ function SVGLine(props) {
28416
28449
  x2: end[0],
28417
28450
  y2: end[1],
28418
28451
  style: style,
28419
- className: dragging ? "movable-line-dragging" : undefined
28452
+ className: dragging ? "movable-dragging" : undefined
28420
28453
  });
28421
28454
  }
28422
28455
 
@@ -28491,7 +28524,7 @@ const StyledMovablePoint = props => {
28491
28524
  }
28492
28525
  }, /*#__PURE__*/React__namespace.createElement("circle", {
28493
28526
  className: "movable-point-hitbox",
28494
- r: 22,
28527
+ r: TARGET_SIZE / 2,
28495
28528
  cx: x,
28496
28529
  cy: y
28497
28530
  }), /*#__PURE__*/React__namespace.createElement("circle", {
@@ -28527,9 +28560,9 @@ const SegmentGraph = props => {
28527
28560
  snaps: snapStep,
28528
28561
  range: range,
28529
28562
  onMoveLine: delta => {
28530
- dispatch(moveSegment(i, delta));
28563
+ dispatch(moveLine(i, delta));
28531
28564
  },
28532
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(i, endpointIndex, destination)),
28565
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28533
28566
  "data-testid": "segment" + i
28534
28567
  })));
28535
28568
  };
@@ -28572,9 +28605,9 @@ const LinearGraph = props => {
28572
28605
  snaps: snapStep,
28573
28606
  range: range,
28574
28607
  onMoveLine: delta => {
28575
- dispatch(moveSegment(i, delta));
28608
+ dispatch(moveLine(i, delta));
28576
28609
  },
28577
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(i, endpointIndex, destination))
28610
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i))
28578
28611
  // "linear" or "linear-system" + index
28579
28612
  ,
28580
28613
  "data-testid": type + i,
@@ -28625,8 +28658,8 @@ const RayGraph = props => {
28625
28658
 
28626
28659
  // a ray only has one line
28627
28660
  const [start, end] = lines[0];
28628
- const handleMoveLine = delta => dispatch(moveSegment(0, delta));
28629
- const handleMovePoint = (newPoint, pointIndex) => dispatch(moveControlPoint(0, pointIndex, newPoint));
28661
+ const handleMoveLine = delta => dispatch(moveLine(0, delta));
28662
+ const handleMovePoint = (newPoint, pointIndex) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
28630
28663
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableLine, {
28631
28664
  start: start,
28632
28665
  end: end,
@@ -28645,6 +28678,62 @@ const RayGraph = props => {
28645
28678
  }));
28646
28679
  };
28647
28680
 
28681
+ const PolygonGraph = props => {
28682
+ const [focused, setFocused] = React__namespace.useState(false);
28683
+ const [hovered, setHovered] = React__namespace.useState(false);
28684
+ const {
28685
+ dispatch
28686
+ } = props;
28687
+ const {
28688
+ coords,
28689
+ type
28690
+ } = props.graphState;
28691
+ const points = coords ?? [[0, 0]];
28692
+ const pointsSum = points.reduce((acc, point) => mafs.vec.add(acc, point), [0, 0]);
28693
+ const midpoint = pointsSum && mafs.vec.scale(pointsSum, 1 / (points.length ?? 1));
28694
+ const ref = React__namespace.useRef(null);
28695
+ const {
28696
+ dragging
28697
+ } = mafs.useMovable({
28698
+ gestureTarget: ref,
28699
+ point: midpoint,
28700
+ onMove: newPoint => {
28701
+ const delta = mafs.vec.sub(newPoint, midpoint);
28702
+ dispatch(moveAll(delta));
28703
+ },
28704
+ constrain: p => p
28705
+ });
28706
+ const active = hovered || focused || dragging;
28707
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28708
+ points: [...points],
28709
+ color: "var(--movable-line-stroke-color)",
28710
+ svgPolygonProps: {
28711
+ strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)"
28712
+ }
28713
+ }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28714
+ points: [...points],
28715
+ color: "transparent",
28716
+ svgPolygonProps: {
28717
+ ref,
28718
+ tabIndex: 0,
28719
+ strokeWidth: TARGET_SIZE,
28720
+ style: {
28721
+ cursor: dragging ? "grabbing" : "grab"
28722
+ },
28723
+ onFocus: () => setFocused(true),
28724
+ onBlur: () => setFocused(false),
28725
+ onMouseEnter: () => setHovered(true),
28726
+ onMouseLeave: () => setHovered(false),
28727
+ className: "movable-polygon"
28728
+ }
28729
+ }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
28730
+ key: i,
28731
+ point: point,
28732
+ onMove: destination => dispatch(moveControlPoint(i, destination)),
28733
+ "data-testid": type + i
28734
+ })));
28735
+ };
28736
+
28648
28737
  const renderLineLabel = (n, _ref) => {
28649
28738
  let [min, max] = _ref;
28650
28739
  return n !== -1 && n !== min && n !== max;
@@ -28660,234 +28749,6 @@ const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React__nam
28660
28749
  yAxis: axisOptions(props, 1)
28661
28750
  });
28662
28751
 
28663
- function interactiveGraphReducer(state, action) {
28664
- switch (action.type) {
28665
- case MOVE_CONTROL_POINT:
28666
- {
28667
- const newCoords = updateAtIndex({
28668
- array: state.coords,
28669
- index: action.objectIndex,
28670
- update: tuple => setAtIndex({
28671
- array: tuple,
28672
- index: action.pointIndex,
28673
- newValue: snap(state, bound$1(state, action.destination))
28674
- })
28675
- });
28676
- if (!validSegments(newCoords)) {
28677
- return state;
28678
- }
28679
- return {
28680
- ...state,
28681
- hasBeenInteractedWith: true,
28682
- coords: newCoords
28683
- };
28684
- }
28685
- case MOVE_LINE:
28686
- {
28687
- const currentLine = state.coords?.[action.lineIndex];
28688
- if (!currentLine) {
28689
- throw new Error("No line to move");
28690
- }
28691
- const maxMoves = currentLine.map(point => maxMove(state, point));
28692
- const minMoves = currentLine.map(point => minMove(state, point));
28693
- const maxXMove = Math.min(...maxMoves.map(move => move[0]));
28694
- const maxYMove = Math.min(...maxMoves.map(move => move[1]));
28695
- const minXMove = Math.max(...minMoves.map(move => move[0]));
28696
- const minYMove = Math.max(...minMoves.map(move => move[1]));
28697
- const dx = clamp(action.delta[0], minXMove, maxXMove);
28698
- const dy = clamp(action.delta[1], minYMove, maxYMove);
28699
- const newValue = [snap(state, mafs.vec.add(currentLine[0], [dx, dy])), snap(state, mafs.vec.add(currentLine[1], [dx, dy]))];
28700
- const newLine = setAtIndex({
28701
- array: state.coords,
28702
- index: action.lineIndex,
28703
- newValue
28704
- });
28705
- return {
28706
- ...state,
28707
- hasBeenInteractedWith: true,
28708
- coords: newLine
28709
- };
28710
- }
28711
- default:
28712
- throw new wonderStuffCore.UnreachableCaseError(action);
28713
- }
28714
- }
28715
- function snap(state, point) {
28716
- const [requestedX, requestedY] = point;
28717
- const [snapX, snapY] = state.snapStep;
28718
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28719
- }
28720
-
28721
- // Returns the closest point to the given `point` that is within the graph
28722
- // bounds given in `state`.
28723
- function bound$1(state, point) {
28724
- const [requestedX, requestedY] = point;
28725
- const [snapX, snapY] = state.snapStep;
28726
- const [[minX, maxX], [minY, maxY]] = state.range;
28727
- return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
28728
- }
28729
-
28730
- // Returns the vector from the given point to the top-right corner of the graph
28731
- function maxMove(state, point) {
28732
- const topRight = bound$1(state, [Infinity, Infinity]);
28733
- return mafs.vec.sub(topRight, point);
28734
- }
28735
-
28736
- // Returns the vector from the given point to the bottom-left corner of the
28737
- // graph
28738
- function minMove(state, point) {
28739
- const bottomLeft = bound$1(state, [-Infinity, -Infinity]);
28740
- return mafs.vec.sub(bottomLeft, point);
28741
- }
28742
- function clamp(value, min, max) {
28743
- if (value < min) {
28744
- return min;
28745
- }
28746
- if (value > max) {
28747
- return max;
28748
- }
28749
- return value;
28750
- }
28751
- function validSegments(segments) {
28752
- return segments.every(_ref => {
28753
- let [start, end] = _ref;
28754
- return !kmath.vector.equal(start, end);
28755
- });
28756
- }
28757
- function updateAtIndex(args) {
28758
- const {
28759
- array = [],
28760
- index,
28761
- update
28762
- } = args;
28763
- const newValue = update(array[index]);
28764
- return setAtIndex({
28765
- array,
28766
- index,
28767
- newValue
28768
- });
28769
- }
28770
- function setAtIndex(args) {
28771
- const {
28772
- array = [],
28773
- index,
28774
- newValue
28775
- } = args;
28776
- const copy = [...array];
28777
- copy[index] = newValue;
28778
- // restoring readonly to array
28779
- return copy;
28780
- }
28781
-
28782
- // same as pointsFromNormalized in interactive-graph.tsx
28783
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
28784
- // maps over [x, y]
28785
- coords.map((coord, i) => {
28786
- const axisRange = range[i];
28787
- if (noSnap) {
28788
- return axisRange[0] + (axisRange[1] - axisRange[0]) * coord;
28789
- }
28790
- const axisStep = step[i];
28791
- const nSteps = Math.floor((axisRange[1] - axisRange[0]) / axisStep);
28792
- const tick = Math.round(coord * nSteps);
28793
- return axisRange[0] + axisStep * tick;
28794
- }));
28795
-
28796
- // same as normalizeCoords in interactive-graph.tsx
28797
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
28798
- const extent = ranges[i][1] - ranges[i][0];
28799
- return (coord + ranges[i][1]) / extent;
28800
- }));
28801
-
28802
- function initializeGraphState(params) {
28803
- const {
28804
- graph,
28805
- step,
28806
- snapStep,
28807
- range
28808
- } = params;
28809
- switch (graph.type) {
28810
- case "segment":
28811
- return {
28812
- type: "segment",
28813
- hasBeenInteractedWith: false,
28814
- range,
28815
- snapStep,
28816
- coords: getDefaultSegments({
28817
- graph,
28818
- step,
28819
- range
28820
- })
28821
- };
28822
- case "linear":
28823
- case "linear-system":
28824
- case "ray":
28825
- return {
28826
- type: graph.type,
28827
- hasBeenInteractedWith: false,
28828
- range,
28829
- snapStep,
28830
- // Linear and ray graphs have a single tuple of points, while a
28831
- // linear system has two tuples of points.
28832
- coords: getLineCoords(graph, range, step)
28833
- };
28834
- }
28835
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
28836
- }
28837
- const getDefaultSegments = props => {
28838
- const ys = n => {
28839
- switch (n) {
28840
- case 2:
28841
- return [5, -5];
28842
- case 3:
28843
- return [5, 0, -5];
28844
- case 4:
28845
- return [6, 2, -2, -6];
28846
- case 5:
28847
- return [6, 3, 0, -3, -6];
28848
- case 6:
28849
- return [5, 3, 1, -1, -3, -5];
28850
- default:
28851
- return [5];
28852
- }
28853
- };
28854
- return ys(props.graph.numSegments).map(y => {
28855
- let endpoints = [[-5, y], [5, y]];
28856
- endpoints = normalizeCoords(endpoints, props.range);
28857
- endpoints = normalizePoints(props.range, props.step, endpoints);
28858
- return endpoints;
28859
- });
28860
- };
28861
-
28862
- // TS v4 doesn't narrow return types, while v5 does.
28863
- // Instead of updating to v5, using generic type to relate input and output types.
28864
- function getGradableGraph(state, initialGraph) {
28865
- if (!state.hasBeenInteractedWith) {
28866
- return initialGraph;
28867
- }
28868
- switch (true) {
28869
- // coords: Array of CollinearTuple
28870
- case state.type === "linear-system" && initialGraph.type === "linear-system":
28871
- case state.type === "segment" && initialGraph.type === "segment":
28872
- return {
28873
- ...initialGraph,
28874
- coords: state.coords
28875
- };
28876
- // coords: CollinearTuple
28877
- case state.type === "linear" && initialGraph.type === "linear":
28878
- case state.type === "ray" && initialGraph.type === "ray":
28879
- return {
28880
- ...initialGraph,
28881
- coords: state.coords?.[0]
28882
- };
28883
- }
28884
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
28885
- }
28886
- const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
28887
- const getLineCoords = (graph, range, step) =>
28888
- // Return two lines for a linear system, one for a ray or linear
28889
- graph.coords ?? graph.type === "linear-system" ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])];
28890
-
28891
28752
  // Displays a stylized open eye: lint warnings are visible
28892
28753
  const VisibleIcon = () => /*#__PURE__*/React__namespace.createElement("svg", {
28893
28754
  width: "24",
@@ -29477,107 +29338,486 @@ const styles$h = aphrodite.StyleSheet.create({
29477
29338
  flexDirection: "row",
29478
29339
  alignItems: "center"
29479
29340
  }
29480
- });
29481
-
29482
- const truth = () => true;
29341
+ });
29342
+
29343
+ const truth = () => true;
29344
+
29345
+ /* A minor abstraction on top of NumberInput for ranges
29346
+ *
29347
+ */
29348
+ class RangeInput extends React__namespace.Component {
29349
+ static propTypes = {
29350
+ value: PropTypes__default["default"].array.isRequired,
29351
+ onChange: PropTypes__default["default"].func.isRequired,
29352
+ placeholder: PropTypes__default["default"].array,
29353
+ checkValidity: PropTypes__default["default"].func
29354
+ };
29355
+ static defaultProps = {
29356
+ placeholder: [null, null]
29357
+ };
29358
+ render() {
29359
+ const value = this.props.value;
29360
+ const checkValidity = this.props.checkValidity || truth;
29361
+ return /*#__PURE__*/React__namespace.createElement("div", {
29362
+ className: "range-input"
29363
+ }, /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29364
+ value: value[0],
29365
+ checkValidity: val => checkValidity([val, value[1]])
29366
+ // eslint-disable-next-line react/jsx-no-bind
29367
+ ,
29368
+ onChange: this.onChange.bind(this, 0),
29369
+ placeholder: this.props.placeholder[0]
29370
+ })), /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29371
+ value: value[1],
29372
+ checkValidity: val => checkValidity([value[0], val])
29373
+ // eslint-disable-next-line react/jsx-no-bind
29374
+ ,
29375
+ onChange: this.onChange.bind(this, 1),
29376
+ placeholder: this.props.placeholder[1]
29377
+ })));
29378
+ }
29379
+ onChange = (i, newVal) => {
29380
+ const value = this.props.value;
29381
+ if (i === 0) {
29382
+ this.props.onChange([newVal, value[1]]);
29383
+ } else {
29384
+ this.props.onChange([value[0], newVal]);
29385
+ }
29386
+ };
29387
+ }
29388
+
29389
+ var components = /*#__PURE__*/Object.freeze({
29390
+ __proto__: null,
29391
+ ButtonGroup: ButtonGroup,
29392
+ Graph: Graph,
29393
+ Graphie: Graphie,
29394
+ HUD: HUD,
29395
+ Icon: Icon,
29396
+ InfoTip: InfoTip,
29397
+ InlineIcon: InlineIcon,
29398
+ MathInput: MathInput,
29399
+ MultiButtonGroup: MultiButtonGroup,
29400
+ NumberInput: NumberInput,
29401
+ PropCheckBox: PropCheckBox,
29402
+ RangeInput: RangeInput,
29403
+ SvgImage: SvgImage,
29404
+ TextInput: TextInput,
29405
+ TextListEditor: TextListEditor
29406
+ });
29407
+
29408
+ /**
29409
+ * If a graphie URL is provided in `backgroundImage`, will return the rendered graphie background.
29410
+ * Otherwise, returns `null`.
29411
+ */
29412
+ const getLegacyGrid = (box, backgroundImage) => {
29413
+ const {
29414
+ url,
29415
+ width,
29416
+ height
29417
+ } = backgroundImage ?? {};
29418
+ if (url && typeof url === "string") {
29419
+ const scale = box[0] / interactiveSizes$1.defaultBoxSize;
29420
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29421
+ style: {
29422
+ position: "absolute",
29423
+ bottom: 0,
29424
+ left: 0
29425
+ }
29426
+ }, /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref => {
29427
+ let {
29428
+ setAssetStatus
29429
+ } = _ref;
29430
+ return /*#__PURE__*/React__namespace.createElement(SvgImage, {
29431
+ src: url,
29432
+ width: width,
29433
+ height: height,
29434
+ scale: scale,
29435
+ responsive: false,
29436
+ setAssetStatus: setAssetStatus,
29437
+ alt: ""
29438
+ });
29439
+ }));
29440
+ }
29441
+ return null;
29442
+ };
29443
+
29444
+ /** Determine if coords is type CollinearTuple[] */
29445
+ const isCollinearTuples = coords => Array.isArray(coords[0][0]);
29446
+
29447
+ // Generic type makes returned state match input state
29448
+ function interactiveGraphReducer(state, action) {
29449
+ const {
29450
+ snapStep,
29451
+ range
29452
+ } = state;
29453
+ if (!state.coords) {
29454
+ throw new Error("Graph state must have been initialized with coords");
29455
+ }
29456
+ switch (action.type) {
29457
+ case MOVE_CONTROL_POINT:
29458
+ {
29459
+ const newCoords = action.itemIndex !== undefined && state.coords && isCollinearTuples(state.coords) ? updateAtIndex({
29460
+ array: state.coords,
29461
+ index: action.itemIndex,
29462
+ update: tuple => setAtIndex({
29463
+ array: tuple,
29464
+ index: action.pointIndex,
29465
+ newValue: snap({
29466
+ snapStep,
29467
+ point: bound$1({
29468
+ snapStep,
29469
+ range,
29470
+ point: action.destination
29471
+ })
29472
+ })
29473
+ })
29474
+ }) : setAtIndex({
29475
+ array: state.coords,
29476
+ index: action.pointIndex,
29477
+ newValue: snap({
29478
+ snapStep,
29479
+ point: bound$1({
29480
+ snapStep,
29481
+ range,
29482
+ point: action.destination
29483
+ })
29484
+ })
29485
+ });
29486
+
29487
+ // Cannot type narrow both conditions within function parameters,
29488
+ // so this may seem redundant, but it's necessary for type safety.
29489
+ const coordsToCheck = isCollinearTuples(newCoords) && action.itemIndex !== undefined ? newCoords[action.itemIndex] : newCoords;
29490
+ if (!isCollinearTuples(coordsToCheck) && coordsOverlap(coordsToCheck)) {
29491
+ return state;
29492
+ }
29493
+ return {
29494
+ ...state,
29495
+ hasBeenInteractedWith: true,
29496
+ coords: newCoords
29497
+ };
29498
+ }
29499
+ case MOVE_LINE:
29500
+ {
29501
+ if (!isCollinearTuples(state.coords)) {
29502
+ throw new Error("Cannot call this action unless coords is array of CollinearTuple.");
29503
+ }
29504
+ if (action.itemIndex === undefined) {
29505
+ throw new Error("Please provide index of line to move");
29506
+ }
29507
+ const currentLine = state.coords?.[action.itemIndex];
29508
+ if (!currentLine) {
29509
+ throw new Error("No line to move");
29510
+ }
29511
+ const change = getChange(currentLine, action.delta, {
29512
+ snapStep,
29513
+ range
29514
+ });
29515
+ const newLine = [snap({
29516
+ snapStep,
29517
+ point: mafs.vec.add(currentLine[0], change)
29518
+ }), snap({
29519
+ snapStep,
29520
+ point: mafs.vec.add(currentLine[1], change)
29521
+ })];
29522
+ const newCoords = setAtIndex({
29523
+ array: state.coords,
29524
+ index: action.itemIndex,
29525
+ newValue: newLine
29526
+ });
29527
+ return {
29528
+ ...state,
29529
+ hasBeenInteractedWith: true,
29530
+ coords: newCoords
29531
+ };
29532
+ }
29533
+ case MOVE_ALL:
29534
+ {
29535
+ if (isCollinearTuples(state.coords)) {
29536
+ throw new Error("Cannot call this action unless coords is array of vectors.");
29537
+ }
29538
+ const change = getChange(state.coords, action.delta, {
29539
+ snapStep,
29540
+ range
29541
+ });
29542
+ const newCoords = state.coords.map(point => snap({
29543
+ snapStep,
29544
+ point: mafs.vec.add(point, change)
29545
+ }));
29546
+ return {
29547
+ ...state,
29548
+ hasBeenInteractedWith: true,
29549
+ coords: newCoords
29550
+ };
29551
+ }
29552
+ default:
29553
+ throw new wonderStuffCore.UnreachableCaseError(action);
29554
+ }
29555
+ }
29556
+ const getChange = (coords, delta, constraintOpts) => {
29557
+ const [deltaX, deltaY] = delta;
29558
+ const maxMoves = coords.map(point => maxMove({
29559
+ ...constraintOpts,
29560
+ point
29561
+ }));
29562
+ const minMoves = coords.map(point => minMove({
29563
+ ...constraintOpts,
29564
+ point
29565
+ }));
29566
+ const maxXMove = Math.min(...maxMoves.map(move => move[0]));
29567
+ const maxYMove = Math.min(...maxMoves.map(move => move[1]));
29568
+ const minXMove = Math.max(...minMoves.map(move => move[0]));
29569
+ const minYMove = Math.max(...minMoves.map(move => move[1]));
29570
+ const dx = clamp(deltaX, minXMove, maxXMove);
29571
+ const dy = clamp(deltaY, minYMove, maxYMove);
29572
+ return [dx, dy];
29573
+ };
29574
+ function snap(_ref) {
29575
+ let {
29576
+ snapStep,
29577
+ point
29578
+ } = _ref;
29579
+ const [requestedX, requestedY] = point;
29580
+ const [snapX, snapY] = snapStep;
29581
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29582
+ }
29583
+
29584
+ // Returns the closest point to the given `point` that is within the graph
29585
+ // bounds given in `state`.
29586
+ function bound$1(_ref2) {
29587
+ let {
29588
+ snapStep,
29589
+ range,
29590
+ point
29591
+ } = _ref2;
29592
+ const [requestedX, requestedY] = point;
29593
+ const [snapX, snapY] = snapStep;
29594
+ const [[minX, maxX], [minY, maxY]] = range;
29595
+ return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
29596
+ }
29597
+
29598
+ // Returns the vector from the given point to the top-right corner of the graph
29599
+ function maxMove(_ref3) {
29600
+ let {
29601
+ snapStep,
29602
+ range,
29603
+ point
29604
+ } = _ref3;
29605
+ const topRight = bound$1({
29606
+ snapStep,
29607
+ range,
29608
+ point: [Infinity, Infinity]
29609
+ });
29610
+ return mafs.vec.sub(topRight, point);
29611
+ }
29612
+
29613
+ // Returns the vector from the given point to the bottom-left corner of the
29614
+ // graph
29615
+ function minMove(_ref4) {
29616
+ let {
29617
+ snapStep,
29618
+ range,
29619
+ point
29620
+ } = _ref4;
29621
+ const bottomLeft = bound$1({
29622
+ snapStep,
29623
+ range,
29624
+ point: [-Infinity, -Infinity]
29625
+ });
29626
+ return mafs.vec.sub(bottomLeft, point);
29627
+ }
29628
+ function clamp(value, min, max) {
29629
+ if (value < min) {
29630
+ return min;
29631
+ }
29632
+ if (value > max) {
29633
+ return max;
29634
+ }
29635
+ return value;
29636
+ }
29637
+ const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && kmath.vector.equal(coord, c)));
29638
+ function updateAtIndex(args) {
29639
+ const {
29640
+ array = [],
29641
+ index,
29642
+ update
29643
+ } = args;
29644
+ const newValue = update(array[index]);
29645
+ return setAtIndex({
29646
+ array,
29647
+ index,
29648
+ newValue
29649
+ });
29650
+ }
29651
+ function setAtIndex(args) {
29652
+ const {
29653
+ array = [],
29654
+ index,
29655
+ newValue
29656
+ } = args;
29657
+ const copy = [...array];
29658
+ copy[index] = newValue;
29659
+ // restoring readonly to array
29660
+ return copy;
29661
+ }
29662
+
29663
+ function initializeGraphState(params) {
29664
+ const {
29665
+ graph,
29666
+ step,
29667
+ snapStep,
29668
+ range
29669
+ } = params;
29670
+ switch (graph.type) {
29671
+ case "segment":
29672
+ return {
29673
+ type: "segment",
29674
+ hasBeenInteractedWith: false,
29675
+ range,
29676
+ snapStep,
29677
+ coords: getDefaultSegments({
29678
+ graph,
29679
+ step,
29680
+ range
29681
+ })
29682
+ };
29683
+ case "linear":
29684
+ case "linear-system":
29685
+ case "ray":
29686
+ return {
29687
+ type: graph.type,
29688
+ hasBeenInteractedWith: false,
29689
+ range,
29690
+ snapStep,
29691
+ // Linear and ray graphs have a single tuple of points, while a
29692
+ // linear system has two tuples of points.
29693
+ coords: getLineCoords({
29694
+ graph,
29695
+ range,
29696
+ step
29697
+ })
29698
+ };
29699
+ case "polygon":
29700
+ return {
29701
+ type: "polygon",
29702
+ hasBeenInteractedWith: false,
29703
+ range,
29704
+ snapStep,
29705
+ coords: getPolygonCoords({
29706
+ graph,
29707
+ range,
29708
+ step
29709
+ })
29710
+ };
29711
+ case "angle":
29712
+ case "point":
29713
+ case "circle":
29714
+ case "sinusoid":
29715
+ case "quadratic":
29716
+ throw new Error("Mafs not yet implemented for graph type: " + graph.type);
29717
+ default:
29718
+ throw new wonderStuffCore.UnreachableCaseError(graph);
29719
+ }
29720
+ }
29483
29721
 
29484
- /* A minor abstraction on top of NumberInput for ranges
29485
- *
29486
- */
29487
- class RangeInput extends React__namespace.Component {
29488
- static propTypes = {
29489
- value: PropTypes__default["default"].array.isRequired,
29490
- onChange: PropTypes__default["default"].func.isRequired,
29491
- placeholder: PropTypes__default["default"].array,
29492
- checkValidity: PropTypes__default["default"].func
29493
- };
29494
- static defaultProps = {
29495
- placeholder: [null, null]
29496
- };
29497
- render() {
29498
- const value = this.props.value;
29499
- const checkValidity = this.props.checkValidity || truth;
29500
- return /*#__PURE__*/React__namespace.createElement("div", {
29501
- className: "range-input"
29502
- }, /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29503
- value: value[0],
29504
- checkValidity: val => checkValidity([val, value[1]])
29505
- // eslint-disable-next-line react/jsx-no-bind
29506
- ,
29507
- onChange: this.onChange.bind(this, 0),
29508
- placeholder: this.props.placeholder[0]
29509
- })), /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
29510
- value: value[1],
29511
- checkValidity: val => checkValidity([value[0], val])
29512
- // eslint-disable-next-line react/jsx-no-bind
29513
- ,
29514
- onChange: this.onChange.bind(this, 1),
29515
- placeholder: this.props.placeholder[1]
29516
- })));
29722
+ // TS v4 doesn't narrow return types, while v5 does.
29723
+ // Instead of updating to v5, using generic type to relate input and output types.
29724
+ function getGradableGraph(state, initialGraph) {
29725
+ if (!state.hasBeenInteractedWith) {
29726
+ return initialGraph;
29517
29727
  }
29518
- onChange = (i, newVal) => {
29519
- const value = this.props.value;
29520
- if (i === 0) {
29521
- this.props.onChange([newVal, value[1]]);
29522
- } else {
29523
- this.props.onChange([value[0], newVal]);
29728
+ switch (true) {
29729
+ // coords: Array of CollinearTuple
29730
+ case state.type === "linear-system" && initialGraph.type === "linear-system":
29731
+ case state.type === "segment" && initialGraph.type === "segment":
29732
+ return {
29733
+ ...initialGraph,
29734
+ coords: state.coords
29735
+ };
29736
+ // coords: CollinearTuple
29737
+ case state.type === "linear" && initialGraph.type === "linear":
29738
+ case state.type === "ray" && initialGraph.type === "ray":
29739
+ return {
29740
+ ...initialGraph,
29741
+ coords: state.coords?.[0]
29742
+ };
29743
+ case state.type === "polygon" && initialGraph.type === "polygon":
29744
+ return {
29745
+ ...initialGraph,
29746
+ coords: state.coords
29747
+ };
29748
+ default:
29749
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29750
+ }
29751
+ }
29752
+ const getDefaultSegments = _ref => {
29753
+ let {
29754
+ graph,
29755
+ range,
29756
+ step
29757
+ } = _ref;
29758
+ const ys = n => {
29759
+ switch (n) {
29760
+ case 2:
29761
+ return [5, -5];
29762
+ case 3:
29763
+ return [5, 0, -5];
29764
+ case 4:
29765
+ return [6, 2, -2, -6];
29766
+ case 5:
29767
+ return [6, 3, 0, -3, -6];
29768
+ case 6:
29769
+ return [5, 3, 1, -1, -3, -5];
29770
+ default:
29771
+ return [5];
29524
29772
  }
29525
29773
  };
29526
- }
29774
+ return ys(graph.numSegments).map(y => {
29775
+ let endpoints = [[-5, y], [5, y]];
29776
+ endpoints = normalizeCoords(endpoints, range);
29777
+ endpoints = normalizePoints(range, step, endpoints);
29778
+ return endpoints;
29779
+ });
29780
+ };
29781
+ const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
29782
+ const getLineCoords = _ref2 => {
29783
+ let {
29784
+ graph,
29785
+ range,
29786
+ step
29787
+ } = _ref2;
29788
+ return (
29789
+ // Return two lines for a linear system, one for a ray or linear
29790
+ graph.coords ?? graph.type === "linear-system" ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
29791
+ );
29792
+ };
29793
+ const getPolygonCoords = _ref3 => {
29794
+ let {
29795
+ graph,
29796
+ range,
29797
+ step
29798
+ } = _ref3;
29799
+ let coords = graph.coords;
29800
+ if (coords) {
29801
+ return coords;
29802
+ }
29803
+ const n = graph.numSides || 3;
29804
+ if (n === "unlimited") {
29805
+ coords = [];
29806
+ } else {
29807
+ const angle = 2 * Math.PI / n;
29808
+ const offset = (1 / n - 1 / 2) * Math.PI;
29527
29809
 
29528
- var components = /*#__PURE__*/Object.freeze({
29529
- __proto__: null,
29530
- ButtonGroup: ButtonGroup,
29531
- Graph: Graph,
29532
- Graphie: Graphie,
29533
- HUD: HUD,
29534
- Icon: Icon,
29535
- InfoTip: InfoTip,
29536
- InlineIcon: InlineIcon,
29537
- MathInput: MathInput,
29538
- MultiButtonGroup: MultiButtonGroup,
29539
- NumberInput: NumberInput,
29540
- PropCheckBox: PropCheckBox,
29541
- RangeInput: RangeInput,
29542
- SvgImage: SvgImage,
29543
- TextInput: TextInput,
29544
- TextListEditor: TextListEditor
29545
- });
29810
+ // TODO(alex): Generalize this to more than just triangles so that
29811
+ // all polygons have whole number side lengths if snapping to sides
29812
+ const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
29546
29813
 
29547
- /**
29548
- * If a graphie URL is provided in `backgroundImage`, will return the rendered graphie background.
29549
- * Otherwise, returns `null`.
29550
- */
29551
- const getLegacyGrid = (box, backgroundImage) => {
29552
- const {
29553
- url,
29554
- width,
29555
- height
29556
- } = backgroundImage ?? {};
29557
- if (url && typeof url === "string") {
29558
- const scale = box[0] / interactiveSizes$1.defaultBoxSize;
29559
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29560
- style: {
29561
- position: "absolute",
29562
- bottom: 0,
29563
- left: 0
29564
- }
29565
- }, /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref => {
29566
- let {
29567
- setAssetStatus
29568
- } = _ref;
29569
- return /*#__PURE__*/React__namespace.createElement(SvgImage, {
29570
- src: url,
29571
- width: width,
29572
- height: height,
29573
- scale: scale,
29574
- responsive: false,
29575
- setAssetStatus: setAssetStatus,
29576
- alt: ""
29577
- });
29578
- }));
29814
+ // Generate coords of a regular polygon with n sides
29815
+ coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
29579
29816
  }
29580
- return null;
29817
+ coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29818
+ const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
29819
+ coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
29820
+ return coords;
29581
29821
  };
29582
29822
 
29583
29823
  const renderGraph = props => {
@@ -29605,6 +29845,11 @@ const renderGraph = props => {
29605
29845
  graphState: state,
29606
29846
  dispatch: dispatch
29607
29847
  });
29848
+ case "polygon":
29849
+ return /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
29850
+ graphState: state,
29851
+ dispatch: dispatch
29852
+ });
29608
29853
  default:
29609
29854
  return new wonderStuffCore.UnreachableCaseError(type);
29610
29855
  }
@@ -29618,8 +29863,8 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
29618
29863
  }));
29619
29864
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29620
29865
  style: {
29621
- width: props.backgroundImage?.width ?? width,
29622
- height: props.backgroundImage?.height ?? height,
29866
+ width,
29867
+ height,
29623
29868
  position: "relative"
29624
29869
  }
29625
29870
  }, legacyGrid, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
@@ -41649,7 +41894,7 @@ const init = function (options) {
41649
41894
 
41650
41895
  // This file is processed by a Rollup plugin (replace) to inject the production
41651
41896
  const libName = "@khanacademy/perseus";
41652
- const libVersion = "20.6.0";
41897
+ const libVersion = "20.8.0";
41653
41898
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
41654
41899
 
41655
41900
  const apiVersion = {
@@ -44297,6 +44542,66 @@ function injectWidgets(content, widgets) {
44297
44542
  return context;
44298
44543
  }
44299
44544
 
44545
+ /* Widgets that have individual answers */
44546
+ const INDIVIDUAL_ANSWER_WIDGETS = ["interactive-graph", "categorizer", "grapher"];
44547
+
44548
+ /* Widgets that are supported for automatic scoring */
44549
+ const SUPPORTED_WIDGETS = ["radio", "numeric-input", "input-number", "expression", ...INDIVIDUAL_ANSWER_WIDGETS];
44550
+ const getWidgetTypeFromWidgetKey = widgetKey => {
44551
+ return widgetKey.split(" ")[0];
44552
+ };
44553
+
44554
+ /* Verify if the perseus item has supported widgets for automatic scoring */
44555
+ const isWrongAnswerSupported = widgetKeys => {
44556
+ return widgetKeys.length !== 0 && widgetKeys.every(widgetKey => SUPPORTED_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey)));
44557
+ };
44558
+
44559
+ /* Verify if the widget key has an individual answer for the coach report view */
44560
+ const shouldHaveIndividualAnswer = widgetKey => {
44561
+ return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey));
44562
+ };
44563
+
44564
+ /* Returns the answer userInput submission for currently supported widgets */
44565
+ // TODO (LEMS-1834): Fix userInput any types to be specific
44566
+ const getAnswerFromUserInput = (widgetType, userInput) => {
44567
+ switch (widgetType) {
44568
+ case "categorizer":
44569
+ return userInput.values;
44570
+ case "input-number":
44571
+ return userInput.currentValue;
44572
+ case "numeric-input":
44573
+ return userInput.currentValue;
44574
+ case "radio":
44575
+ return userInput.choicesSelected;
44576
+ }
44577
+ return userInput;
44578
+ };
44579
+
44580
+ /* Returns the correct answer for a given widget key and Perseus Item */
44581
+ // TODO (LEMS-1835): We should fix the resonse type from getWidget to be specific.
44582
+ // TODO (LEMS-1836): We should also consider adding the getOneCorrectAnswerFromRubric method to all widgets.
44583
+ const getCorrectAnswerForWidgetKey = (widgetKey, itemData) => {
44584
+ const rubric = itemData.question.widgets[widgetKey].options;
44585
+ const widgetType = getWidgetTypeFromWidgetKey(widgetKey);
44586
+ const widget = getWidget(widgetType);
44587
+
44588
+ // @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType<any>'.
44589
+ return widget?.getOneCorrectAnswerFromRubric?.(rubric);
44590
+ };
44591
+
44592
+ /* Verify if the widget key exists in the content string of the Perseus Item */
44593
+ const isWidgetKeyInContent = (perseusItem, widgetKey) => {
44594
+ return perseusItem.question.content.indexOf(widgetKey) !== -1;
44595
+ };
44596
+
44597
+ /* Return an array of all the widget keys that exist in the content string of a Perseus Item */
44598
+ const getValidWidgetKeys = perseusItem => {
44599
+ const {
44600
+ widgets
44601
+ } = perseusItem.question;
44602
+ return wonderStuffCore.keys(widgets).filter(key => isWidgetKeyInContent(perseusItem, key));
44603
+ };
44604
+
44300
44605
  /**
44301
44606
  * Helper to parse PerseusItem JSON
44302
44607
  * Why not just use JSON.parse? We want:
@@ -44417,6 +44722,153 @@ function getWidgetTypeFromWidgetId(widgetId) {
44417
44722
  return widgetType;
44418
44723
  }
44419
44724
 
44725
+ let WidgetType = /*#__PURE__*/function (WidgetType) {
44726
+ WidgetType["Categorizer"] = "categorizer";
44727
+ WidgetType["CSProgram"] = "cs-program";
44728
+ WidgetType["Definition"] = "definition";
44729
+ WidgetType["Dropdown"] = "dropdown";
44730
+ WidgetType["Explanation"] = "explanation";
44731
+ WidgetType["Expression"] = "expression";
44732
+ WidgetType["GradedGroup"] = "graded-group";
44733
+ WidgetType["GradedGroupAnswerBar"] = "graded-group-answer-bar";
44734
+ WidgetType["GradedGroupSet"] = "graded-group-set";
44735
+ WidgetType["Grapher"] = "grapher";
44736
+ WidgetType["Group"] = "group";
44737
+ WidgetType["Iframe"] = "iframe";
44738
+ WidgetType["Image"] = "image";
44739
+ WidgetType["InputNumber"] = "input-number";
44740
+ WidgetType["Interaction"] = "interaction";
44741
+ WidgetType["InteractiveGraph"] = "interactive-graph";
44742
+ WidgetType["InteractiveNumberLine"] = "interactive-number-line";
44743
+ WidgetType["LabelImage"] = "label-image";
44744
+ WidgetType["Matcher"] = "matcher";
44745
+ WidgetType["Matrix"] = "matrix";
44746
+ WidgetType["Measurer"] = "measurer";
44747
+ WidgetType["Molecule"] = "molecule";
44748
+ WidgetType["NumberLine"] = "number-line";
44749
+ WidgetType["NumericInput"] = "numeric-input";
44750
+ WidgetType["Orderer"] = "orderer";
44751
+ WidgetType["Passage"] = "passage";
44752
+ WidgetType["PassageRef"] = "passage-ref";
44753
+ WidgetType["PassageRefTarget"] = "passage-ref-target";
44754
+ WidgetType["Plotter"] = "plotter";
44755
+ WidgetType["Protractor"] = "protractor";
44756
+ WidgetType["PythonProgram"] = "python-program";
44757
+ WidgetType["Radio"] = "radio";
44758
+ WidgetType["Sorter"] = "sorter";
44759
+ WidgetType["Table"] = "table";
44760
+ WidgetType["Unit"] = "unit";
44761
+ WidgetType["Video"] = "video";
44762
+ WidgetType["VideoTranscriptLink"] = "video-transcript-link";
44763
+ return WidgetType;
44764
+ }({});
44765
+ // These are all the current widgets that require user-input and are supported for automatic scoring
44766
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44767
+
44768
+ /**
44769
+ * Regex for widget placeholders in a string.
44770
+ *
44771
+ * First capture group is the widget ID (ex. 'radio 1')
44772
+ * Second capture group is the widget type (ex. "radio)
44773
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44774
+ */
44775
+ function getWidgetRegex() {
44776
+ return /\[\[☃ (([a-z-]+) \d+)\]\]/g;
44777
+ }
44778
+
44779
+ /**
44780
+ * Add a widget placeholder using the provided widget type and instance number.
44781
+ * ex. addWidget(WidgetType.Radio, 1) => "[[☃ radio 1]]"
44782
+ *
44783
+ * @param {WidgetType} widgetType
44784
+ * @param {number} instance
44785
+ * @returns {string}
44786
+ */
44787
+ function addWidget(widgetType, instance) {
44788
+ return `[[☃ ${widgetType} ${String(instance)}]]`;
44789
+ }
44790
+
44791
+ /**
44792
+ * Extract all widget IDs, which includes the widget type and instance number.
44793
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44794
+ *
44795
+ * Content should contain Perseus widget placeholders,
44796
+ * which look like: '[[☃ radio 1]]'.
44797
+ *
44798
+ * @param {string} content
44799
+ * @returns {Array<WidgetId>} widgets
44800
+ */
44801
+ function getAllWidgetIds(content) {
44802
+ const widgets = [];
44803
+ const localWidgetRegex = getWidgetRegex();
44804
+ let match = localWidgetRegex.exec(content);
44805
+ while (match !== null) {
44806
+ widgets.push(match[1]);
44807
+ match = localWidgetRegex.exec(content);
44808
+ }
44809
+ return widgets;
44810
+ }
44811
+
44812
+ /**
44813
+ * Check if a specific widget is an actual widget type using its name.
44814
+ * The widget name should include a widget type, without an instance number,
44815
+ * as a string. ex. 'radio'
44816
+ *
44817
+ * @param {string} widgetName
44818
+ * @returns {boolean}
44819
+ */
44820
+ function isWidgetType(widgetName) {
44821
+ return Object.values(WidgetType).includes(widgetName);
44822
+ }
44823
+
44824
+ /**
44825
+ * Extract all widget types from a Perseus JSON content string.
44826
+ * This does not include the instance number and prevents duplicates.
44827
+ * example output: ['radio', 'categorizer']
44828
+ *
44829
+ * @param {string} content
44830
+ * @returns {Array<WidgetId>}
44831
+ */
44832
+ function getAllWidgetTypes(content) {
44833
+ const widgetTypes = [];
44834
+ const localWidgetRegex = getWidgetRegex();
44835
+ let match = localWidgetRegex.exec(content);
44836
+ while (match !== null) {
44837
+ const matchType = match[2];
44838
+ if (!widgetTypes.includes(matchType) && isWidgetType(matchType)) {
44839
+ widgetTypes.push(matchType);
44840
+ }
44841
+ match = localWidgetRegex.exec(content);
44842
+ }
44843
+ return widgetTypes;
44844
+ }
44845
+
44846
+ /**
44847
+ * Check if a specific widget is a question widget type using its ID.
44848
+ * The widget ID includes the widget type and the instance number.
44849
+ *
44850
+ * @param {WidgetId} widgetId
44851
+ * @returns {boolean}
44852
+ */
44853
+ function isQuestionWidgetType(widgetId) {
44854
+ return QUESTION_WIDGETS.includes(widgetId.split(" ")[0]);
44855
+ }
44856
+
44857
+ /**
44858
+ * Extract the widget IDs of all widgets considered question widgets. Widget IDs
44859
+ * include widget type and instance number.
44860
+ * example output: ['radio 1', 'categorizer 1']
44861
+ *
44862
+ * Content should contain Perseus widget placeholders,
44863
+ * which look like: '[[☃ radio 1]]'.
44864
+ *
44865
+ * @param {string} content
44866
+ * @returns Array<WidgetId>
44867
+ */
44868
+ function getQuestionWidgetIds(content) {
44869
+ return getAllWidgetIds(content).filter(isQuestionWidgetType);
44870
+ }
44871
+
44420
44872
  const EditorJsonify = {
44421
44873
  serialize: function () {
44422
44874
  // Omit props that get passed to all widgets
@@ -44458,6 +44910,7 @@ exports.PerseusError = PerseusError;
44458
44910
  exports.PerseusExpressionAnswerFormConsidered = PerseusExpressionAnswerFormConsidered;
44459
44911
  exports.PerseusMarkdown = PerseusMarkdown;
44460
44912
  exports.PlotterWidget = Plotter$1;
44913
+ exports.QUESTION_WIDGETS = QUESTION_WIDGETS;
44461
44914
  exports.Radio = Radio;
44462
44915
  exports.Renderer = Renderer;
44463
44916
  exports.ServerItemRenderer = ref;
@@ -44466,7 +44919,9 @@ exports.TableWidget = Table$1;
44466
44919
  exports.Util = Util;
44467
44920
  exports.WIDGET_PROP_DENYLIST = denylist;
44468
44921
  exports.WidgetJsonifyDeprecated = WidgetJsonifyDeprecated;
44922
+ exports.WidgetType = WidgetType;
44469
44923
  exports.Widgets = widgets$1;
44924
+ exports.addWidget = addWidget;
44470
44925
  exports.apiVersion = apiVersion;
44471
44926
  exports.buildEmptyItemTreeForShape = buildEmptyItemTreeForShape;
44472
44927
  exports.buildMapper = buildMapper;
@@ -44476,8 +44931,15 @@ exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
44476
44931
  exports.displaySigFigs = displaySigFigs;
44477
44932
  exports.generateTestPerseusItem = generateTestPerseusItem;
44478
44933
  exports.genericPerseusItemData = genericPerseusItemData;
44934
+ exports.getAllWidgetIds = getAllWidgetIds;
44935
+ exports.getAllWidgetTypes = getAllWidgetTypes;
44936
+ exports.getAnswerFromUserInput = getAnswerFromUserInput;
44479
44937
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
44938
+ exports.getCorrectAnswerForWidgetKey = getCorrectAnswerForWidgetKey;
44939
+ exports.getQuestionWidgetIds = getQuestionWidgetIds;
44940
+ exports.getValidWidgetKeys = getValidWidgetKeys;
44480
44941
  exports.getWidgetTypeFromWidgetId = getWidgetTypeFromWidgetId;
44942
+ exports.getWidgetTypeFromWidgetKey = getWidgetTypeFromWidgetKey;
44481
44943
  exports.globalConstants = globalConstants;
44482
44944
  exports.globalStyles = globalStyles;
44483
44945
  exports.icons = iconPaths;
@@ -44485,6 +44947,8 @@ exports.init = init;
44485
44947
  exports.injectWidgets = injectWidgets;
44486
44948
  exports.interactiveSizes = interactiveSizes$1;
44487
44949
  exports.isItemRenderableByVersion = isItemRenderableByVersion;
44950
+ exports.isWidgetKeyInContent = isWidgetKeyInContent;
44951
+ exports.isWrongAnswerSupported = isWrongAnswerSupported;
44488
44952
  exports.itemDataVersion = itemDataVersion;
44489
44953
  exports.itemToTree = itemToTree;
44490
44954
  exports.itemVersion = ItemVersion;
@@ -44495,6 +44959,7 @@ exports.plotterPlotTypes = plotterPlotTypes;
44495
44959
  exports.preprocessTex = preprocessTex;
44496
44960
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
44497
44961
  exports.shapes = shapes;
44962
+ exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
44498
44963
  exports.traverse = traverse;
44499
44964
  exports.violatingWidgets = violatingWidgets;
44500
44965
  exports.widgets = allWidgets;