@khanacademy/perseus 20.8.0 → 21.0.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
@@ -12,9 +12,8 @@ var classNames = require('classnames');
12
12
  var React = require('react');
13
13
  var _ = require('underscore');
14
14
  var Clickable = require('@khanacademy/wonder-blocks-clickable');
15
- var Color = require('@khanacademy/wonder-blocks-color');
16
15
  var wonderBlocksPopover = require('@khanacademy/wonder-blocks-popover');
17
- var Spacing = require('@khanacademy/wonder-blocks-spacing');
16
+ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
18
17
  var aphrodite = require('aphrodite');
19
18
  var $ = require('jquery');
20
19
  var PropTypes = require('prop-types');
@@ -33,7 +32,6 @@ var caretDown = require('@phosphor-icons/core/assets/regular/caret-down.svg');
33
32
  var caretUp = require('@phosphor-icons/core/assets/regular/caret-up.svg');
34
33
  var debounce$1 = require('lodash.debounce');
35
34
  var mafs = require('mafs');
36
- var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
37
35
  var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
38
36
  var questionIcon = require('@phosphor-icons/core/regular/question.svg');
39
37
  var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
@@ -73,8 +71,6 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
73
71
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
74
72
  var ___default = /*#__PURE__*/_interopDefaultLegacy(_);
75
73
  var Clickable__default = /*#__PURE__*/_interopDefaultLegacy(Clickable);
76
- var Color__default = /*#__PURE__*/_interopDefaultLegacy(Color);
77
- var Spacing__default = /*#__PURE__*/_interopDefaultLegacy(Spacing);
78
74
  var $__default = /*#__PURE__*/_interopDefaultLegacy($);
79
75
  var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
80
76
  var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM);
@@ -361,16 +357,16 @@ const MathInputIcon = _ref => {
361
357
  focused,
362
358
  active
363
359
  } = _ref;
364
- let color;
360
+ let fillColor;
365
361
  switch (true) {
366
362
  case focused || active:
367
- color = Color__default["default"].white;
363
+ fillColor = wonderBlocksTokens.color.white;
368
364
  break;
369
365
  case hovered:
370
- color = Color__default["default"].blue;
366
+ fillColor = wonderBlocksTokens.color.blue;
371
367
  break;
372
368
  default:
373
- color = Color__default["default"].offBlack;
369
+ fillColor = wonderBlocksTokens.color.offBlack;
374
370
  break;
375
371
  }
376
372
  const dynamicClass = active || focused ? styles$D.iconActive : styles$D.iconInactive;
@@ -380,7 +376,7 @@ const MathInputIcon = _ref => {
380
376
  xmlns: "http://www.w3.org/2000/svg",
381
377
  width: "16",
382
378
  height: "16",
383
- fill: color,
379
+ fill: fillColor,
384
380
  viewBox: "0 0 256 256"
385
381
  }, /*#__PURE__*/React__namespace.createElement("path", {
386
382
  d: "M112,72a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16h64A8,8,0,0,1,112,72Zm-8,104H80V152a8,8,0,0,0-16,0v24H40a8,8,0,0,0,0,16H64v24a8,8,0,0,0,16,0V192h24a8,8,0,0,0,0-16Zm48,0h64a8,8,0,0,0,0-16H152a8,8,0,0,0,0,16Zm64,16H152a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16Zm-61.66-90.34a8,8,0,0,0,11.32,0L184,83.31l18.34,18.35a8,8,0,0,0,11.32-11.32L195.31,72l18.35-18.34a8,8,0,0,0-11.32-11.32L184,60.69,165.66,42.34a8,8,0,0,0-11.32,11.32L172.69,72,154.34,90.34A8,8,0,0,0,154.34,101.66Z"
@@ -417,7 +413,7 @@ const mapButtonSets = buttonSets => {
417
413
  };
418
414
  const inputFocused = {
419
415
  borderWidth: 2,
420
- borderColor: Color__default["default"].blue,
416
+ borderColor: wonderBlocksTokens.color.blue,
421
417
  margin: -1
422
418
  };
423
419
  const styles$D = aphrodite.StyleSheet.create({
@@ -425,35 +421,35 @@ const styles$D = aphrodite.StyleSheet.create({
425
421
  display: "flex",
426
422
  justifyContent: "center",
427
423
  height: "100%",
428
- padding: Spacing__default["default"].xxxSmall_4,
424
+ padding: wonderBlocksTokens.spacing.xxxSmall_4,
429
425
  borderRadius: 1
430
426
  },
431
427
  iconInactive: {
432
- backgroundColor: Color__default["default"].offBlack8
428
+ backgroundColor: wonderBlocksTokens.color.offBlack8
433
429
  },
434
430
  iconActive: {
435
- backgroundColor: Color__default["default"].offBlack64
431
+ backgroundColor: wonderBlocksTokens.color.offBlack64
436
432
  },
437
433
  outerWrapper: {
438
434
  display: "inline-block",
439
435
  borderStyle: "solid",
440
436
  borderWidth: 1,
441
- borderColor: Color__default["default"].offBlack50,
437
+ borderColor: wonderBlocksTokens.color.offBlack50,
442
438
  borderRadius: 3,
443
- background: Color__default["default"].white,
439
+ background: wonderBlocksTokens.color.white,
444
440
  ":hover": inputFocused
445
441
  },
446
442
  wrapperFocused: inputFocused,
447
443
  wrapperError: {
448
- borderColor: Color__default["default"].red,
449
- background: Color.fade(Color__default["default"].red, 0.08),
444
+ borderColor: wonderBlocksTokens.color.red,
445
+ background: wonderBlocksTokens.color.fadedRed8,
450
446
  ":hover": {
451
- borderColor: Color__default["default"].red
447
+ borderColor: wonderBlocksTokens.color.red
452
448
  }
453
449
  },
454
450
  popoverContent: {
455
451
  padding: 0,
456
- paddingBottom: Spacing__default["default"].xxSmall_6,
452
+ paddingBottom: wonderBlocksTokens.spacing.xxSmall_6,
457
453
  maxWidth: "initial"
458
454
  }
459
455
  });
@@ -18894,7 +18890,7 @@ const isAccessible = widgetInfo => {
18894
18890
  }
18895
18891
  return !!accessible;
18896
18892
  };
18897
- const getAllWidgetTypes$1 = () => {
18893
+ const getAllWidgetTypes = () => {
18898
18894
  return ___default["default"].keys(widgets);
18899
18895
  };
18900
18896
  const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
@@ -19166,7 +19162,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19166
19162
  getVersionVector: getVersionVector,
19167
19163
  getPublicWidgets: getPublicWidgets,
19168
19164
  isAccessible: isAccessible,
19169
- getAllWidgetTypes: getAllWidgetTypes$1,
19165
+ getAllWidgetTypes: getAllWidgetTypes,
19170
19166
  upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
19171
19167
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
19172
19168
  traverseChildWidgets: traverseChildWidgets$3,
@@ -19564,6 +19560,9 @@ class Renderer extends React__namespace.Component {
19564
19560
  if (!widgetInfo.type || !widgetInfo.alignment) {
19565
19561
  const newValues = {};
19566
19562
  if (!widgetInfo.type) {
19563
+ // TODO: why does widget have no type?
19564
+ // We don't want to derive type from widget ID
19565
+ // see: LEMS-1845
19567
19566
  newValues.type = widgetId.split(" ")[0];
19568
19567
  }
19569
19568
  if (!widgetInfo.alignment) {
@@ -21863,13 +21862,13 @@ const styles$A = aphrodite.StyleSheet.create({
21863
21862
  width: 40
21864
21863
  },
21865
21864
  leftAlign: {
21866
- paddingLeft: Spacing__default["default"].xxxSmall_4,
21865
+ paddingLeft: wonderBlocksTokens.spacing.xxxSmall_4,
21867
21866
  paddingRight: 0
21868
21867
  },
21869
21868
  rightAlign: {
21870
21869
  textAlign: "right",
21871
21870
  paddingLeft: 0,
21872
- paddingRight: Spacing__default["default"].xxxSmall_4
21871
+ paddingRight: wonderBlocksTokens.spacing.xxxSmall_4
21873
21872
  },
21874
21873
  answerStateUnanswered: {
21875
21874
  backgroundColor: "#eee",
@@ -22966,17 +22965,17 @@ function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, cor
22966
22965
  let borderColor;
22967
22966
  let color;
22968
22967
  if (!showCorrectness && pressed) {
22969
- borderColor = Color__default["default"].blue;
22970
- color = Color__default["default"].blue;
22968
+ borderColor = wonderBlocksTokens.color.blue;
22969
+ color = wonderBlocksTokens.color.blue;
22971
22970
  backgroundColor = "transparent";
22972
22971
  } else if (checked) {
22973
- const bg = showCorrectness && correct ? Color__default["default"].green : Color__default["default"].blue;
22972
+ const bg = showCorrectness && correct ? wonderBlocksTokens.color.green : wonderBlocksTokens.color.blue;
22974
22973
  color = white$1;
22975
22974
  backgroundColor = bg;
22976
22975
  borderColor = bg;
22977
22976
  } else {
22978
- borderColor = Color__default["default"].offBlack64;
22979
- color = Color__default["default"].offBlack64;
22977
+ borderColor = wonderBlocksTokens.color.offBlack64;
22978
+ color = wonderBlocksTokens.color.offBlack64;
22980
22979
  }
22981
22980
 
22982
22981
  // define shape
@@ -23010,7 +23009,7 @@ const ChoiceIcon = function (props) {
23010
23009
  return /*#__PURE__*/React__namespace.createElement("div", {
23011
23010
  className: aphrodite.css(styles$y.iconWrapper)
23012
23011
  }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
23013
- color: Color__default["default"].blue,
23012
+ color: wonderBlocksTokens.color.blue,
23014
23013
  visible: focused || hovered,
23015
23014
  multipleSelect: multipleSelect
23016
23015
  }, /*#__PURE__*/React__namespace.createElement("div", {
@@ -23075,9 +23074,9 @@ const styles$w = aphrodite.StyleSheet.create({
23075
23074
  color: gray68
23076
23075
  },
23077
23076
  circleIncorrectAnswered: {
23078
- backgroundColor: Color__default["default"].red,
23079
- borderColor: Color__default["default"].red,
23080
- color: Color__default["default"].white
23077
+ backgroundColor: wonderBlocksTokens.color.red,
23078
+ borderColor: wonderBlocksTokens.color.red,
23079
+ color: wonderBlocksTokens.color.white
23081
23080
  }
23082
23081
  });
23083
23082
 
@@ -23145,13 +23144,13 @@ const styles$v = aphrodite.StyleSheet.create({
23145
23144
  textTransform: "uppercase"
23146
23145
  },
23147
23146
  correct: {
23148
- color: Color__default["default"].green
23147
+ color: wonderBlocksTokens.color.green
23149
23148
  },
23150
23149
  incorrectAnswered: {
23151
- color: Color__default["default"].red
23150
+ color: wonderBlocksTokens.color.red
23152
23151
  },
23153
23152
  incorrect: {
23154
- color: Color__default["default"].offBlack64
23153
+ color: wonderBlocksTokens.color.offBlack64
23155
23154
  }
23156
23155
  });
23157
23156
 
@@ -23225,7 +23224,7 @@ const Choice = function (props) {
23225
23224
  // @ts-expect-error - TS2322 - Type '{ dispay: string; flexDirection: "column"; color: string; }' is not assignable to type 'Properties<string | number, string & {}>'.
23226
23225
  dispay: "flex",
23227
23226
  flexDirection: "column",
23228
- color: Color__default["default"].offBlack
23227
+ color: wonderBlocksTokens.color.offBlack
23229
23228
  },
23230
23229
  className: descriptionClassName
23231
23230
  }, /*#__PURE__*/React__namespace.createElement("div", {
@@ -23268,7 +23267,7 @@ const Choice = function (props) {
23268
23267
  disabled: disabled || apiOptions.readOnly,
23269
23268
  style: {
23270
23269
  flex: 1,
23271
- color: Color__default["default"].offBlack,
23270
+ color: wonderBlocksTokens.color.offBlack,
23272
23271
  userSelect: "text"
23273
23272
  },
23274
23273
  ref: forwardedRef,
@@ -23285,9 +23284,9 @@ const Choice = function (props) {
23285
23284
  flexDirection: "row",
23286
23285
  justifyContent: "center",
23287
23286
  alignContent: "center",
23288
- paddingTop: Spacing__default["default"].xSmall_8,
23289
- paddingBottom: Spacing__default["default"].xSmall_8,
23290
- paddingLeft: Spacing__default["default"].xSmall_8
23287
+ paddingTop: wonderBlocksTokens.spacing.xSmall_8,
23288
+ paddingBottom: wonderBlocksTokens.spacing.xSmall_8,
23289
+ paddingLeft: wonderBlocksTokens.spacing.xSmall_8
23291
23290
  }
23292
23291
  }, /*#__PURE__*/React__namespace.createElement(ChoiceIcon, {
23293
23292
  pos: pos,
@@ -23303,7 +23302,7 @@ const Choice = function (props) {
23303
23302
  previouslyAnswered: previouslyAnswered
23304
23303
  }), /*#__PURE__*/React__namespace.createElement("span", {
23305
23304
  style: {
23306
- paddingLeft: Spacing__default["default"].small_12,
23305
+ paddingLeft: wonderBlocksTokens.spacing.small_12,
23307
23306
  textAlign: "left",
23308
23307
  flex: 1,
23309
23308
  paddingTop: 4
@@ -23326,7 +23325,7 @@ const Choice = function (props) {
23326
23325
  content: i18n__namespace._("Cross out option"),
23327
23326
  closeButtonVisible: true,
23328
23327
  actions: /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
23329
- size: Spacing__default["default"].medium_16
23328
+ size: wonderBlocksTokens.spacing.medium_16
23330
23329
  }), /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
23331
23330
  kind: "primary",
23332
23331
  "aria-label": i18n__namespace._("Cross out Choice %(letter)s", {
@@ -23374,7 +23373,7 @@ const Choice = function (props) {
23374
23373
  return /*#__PURE__*/React__namespace.createElement(Icon, {
23375
23374
  icon: ellipsisHorizontalIcon,
23376
23375
  size: 3,
23377
- color: Color__default["default"].offBlack64
23376
+ color: wonderBlocksTokens.color.offBlack64
23378
23377
  });
23379
23378
  });
23380
23379
  })), showRationale && /*#__PURE__*/React__namespace.createElement("div", {
@@ -23392,7 +23391,7 @@ const styles$u = aphrodite.StyleSheet.create({
23392
23391
  padding: intermediateCheckboxPadding,
23393
23392
  paddingTop: 0,
23394
23393
  marginLeft: 54,
23395
- color: Color__default["default"].offBlack64,
23394
+ color: wonderBlocksTokens.color.offBlack64,
23396
23395
  [mediaQueries.smOrSmaller]: {
23397
23396
  padding: intermediateCheckboxPaddingPhone,
23398
23397
  paddingTop: 0
@@ -24818,8 +24817,8 @@ class Definition extends React__namespace.Component {
24818
24817
  } = _ref2;
24819
24818
  return /*#__PURE__*/React__namespace.createElement("span", {
24820
24819
  style: {
24821
- color: Color__default["default"].blue,
24822
- borderBottom: hovered || focused || pressed ? `2px solid ${Color__default["default"].blue}` : "none"
24820
+ color: wonderBlocksTokens.color.blue,
24821
+ borderBottom: hovered || focused || pressed ? `2px solid ${wonderBlocksTokens.color.blue}` : "none"
24823
24822
  }
24824
24823
  }, this.props.togglePrompt);
24825
24824
  }));
@@ -24828,7 +24827,7 @@ class Definition extends React__namespace.Component {
24828
24827
  }
24829
24828
  const styles$q = {
24830
24829
  tooltipBody: {
24831
- color: Color__default["default"].offBlack,
24830
+ color: wonderBlocksTokens.color.offBlack,
24832
24831
  fontSize: 20,
24833
24832
  fontWeight: 500,
24834
24833
  lineHeight: "30px"
@@ -25126,7 +25125,7 @@ class GradedGroupAnswerBar extends React__namespace.Component {
25126
25125
  } = this.props;
25127
25126
  const answerBarStyle = {
25128
25127
  ...styles$o.answerBar,
25129
- backgroundColor: answerBarState === "CORRECT" ? Color__default["default"].offWhite : "white",
25128
+ backgroundColor: answerBarState === "CORRECT" ? wonderBlocksTokens.color.offWhite : "white",
25130
25129
  // Center the "Correct!" message only when there's no next question
25131
25130
  justifyContent: answerBarState === "CORRECT" && !onNextQuestion ? "center" : "space-between"
25132
25131
  };
@@ -25156,7 +25155,7 @@ class GradedGroupAnswerBar extends React__namespace.Component {
25156
25155
  }, /*#__PURE__*/React__namespace.createElement("span", {
25157
25156
  style: {
25158
25157
  fontSize: 28,
25159
- color: Color__default["default"].green
25158
+ color: wonderBlocksTokens.color.green
25160
25159
  }
25161
25160
  }, /*#__PURE__*/React__namespace.createElement(InlineIcon, _extends({}, iconStar, {
25162
25161
  style: {
@@ -25186,7 +25185,7 @@ const styles$o = {
25186
25185
  marginTop: phoneMargin,
25187
25186
  paddingLeft: phoneMargin,
25188
25187
  paddingRight: 10,
25189
- borderTop: `1px solid ${Color__default["default"].offBlack50}`
25188
+ borderTop: `1px solid ${wonderBlocksTokens.color.offBlack50}`
25190
25189
  },
25191
25190
  tryAgainIcon: {
25192
25191
  fontSize: 28,
@@ -25494,7 +25493,7 @@ const styles$n = aphrodite.StyleSheet.create({
25494
25493
  padding: 0,
25495
25494
  border: "none",
25496
25495
  marginTop: 20,
25497
- color: Color__default["default"].blue,
25496
+ color: wonderBlocksTokens.color.blue,
25498
25497
  cursor: "pointer",
25499
25498
  display: "block",
25500
25499
  clear: "both"
@@ -25502,7 +25501,7 @@ const styles$n = aphrodite.StyleSheet.create({
25502
25501
  explanationTitle: {
25503
25502
  backgroundColor: "unset",
25504
25503
  marginTop: 20,
25505
- color: Color__default["default"].blue,
25504
+ color: wonderBlocksTokens.color.blue,
25506
25505
  marginBottom: 10,
25507
25506
  cursor: "pointer",
25508
25507
  fontSize: 14,
@@ -25718,7 +25717,7 @@ const styles$m = aphrodite.StyleSheet.create({
25718
25717
  },
25719
25718
  title: {
25720
25719
  fontSize: 12,
25721
- color: Color__default["default"].offBlack64,
25720
+ color: wonderBlocksTokens.color.offBlack64,
25722
25721
  textTransform: "uppercase",
25723
25722
  marginBottom: 11,
25724
25723
  letterSpacing: 0.8
@@ -25752,7 +25751,7 @@ const styles$m = aphrodite.StyleSheet.create({
25752
25751
  height: 10,
25753
25752
  borderRadius: "100%",
25754
25753
  borderWidth: 2,
25755
- borderColor: Color__default["default"].blue,
25754
+ borderColor: wonderBlocksTokens.color.blue,
25756
25755
  borderStyle: "solid"
25757
25756
  },
25758
25757
  indicatorDotFocused: {
@@ -25760,7 +25759,7 @@ const styles$m = aphrodite.StyleSheet.create({
25760
25759
  borderStyle: "double"
25761
25760
  },
25762
25761
  indicatorDotActive: {
25763
- backgroundColor: Color__default["default"].blue,
25762
+ backgroundColor: wonderBlocksTokens.color.blue,
25764
25763
  width: "100%",
25765
25764
  height: "100%"
25766
25765
  },
@@ -28370,6 +28369,61 @@ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.
28370
28369
  return (coord + ranges[i][1]) / extent;
28371
28370
  }));
28372
28371
 
28372
+ const useTransform = function () {
28373
+ const {
28374
+ viewTransform,
28375
+ userTransform
28376
+ } = mafs.useTransformContext();
28377
+ const transformToPx = mafs.vec.matrixMult(viewTransform, userTransform);
28378
+ for (var _len = arguments.length, points = new Array(_len), _key = 0; _key < _len; _key++) {
28379
+ points[_key] = arguments[_key];
28380
+ }
28381
+ const transformedPoints = points.map(point => mafs.vec.transform(point, transformToPx));
28382
+ return transformedPoints;
28383
+ };
28384
+
28385
+ /**
28386
+ * Given two points, find the tips that extends through the points to the edge of the range.
28387
+ * @param collinearPoint - The point that the line passes through. Needed to establish slope.
28388
+ * @param extendFrom - The point that the line extends from to the edge of the graph.
28389
+ */
28390
+ const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
28391
+ // edges of the graph
28392
+ const [[xMin, xMax], [yMin, yMax]] = range;
28393
+ const [aX, aY] = collinearPoint;
28394
+ const [bX, bY] = extendFrom;
28395
+ const yDiff = bY - aY;
28396
+ const xDiff = bX - aX;
28397
+ const slope = yDiff / xDiff;
28398
+ const yAtXMin = slope * (xMin - aX) + aY;
28399
+ const yAtXMax = slope * (xMax - aX) + aY;
28400
+ const xAtYMin = (yMin - aY) / slope + aX;
28401
+ const xAtYMax = (yMax - aY) / slope + aX;
28402
+
28403
+ // clock analogy to describe quadrants
28404
+ switch (true) {
28405
+ // 12 o'clock to 2:59
28406
+ case yDiff > 0 && xDiff >= 0:
28407
+ return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
28408
+ // 3 o'clock to 5:59
28409
+ case yDiff <= 0 && xDiff > 0:
28410
+ // xAtYMin evaluates to -Infinity here, so we use absolute value
28411
+ return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
28412
+ // 9 o'clock to 11:59
28413
+ case yDiff >= 0 && xDiff < 0:
28414
+ return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
28415
+ // 6 o'clock to 8:59
28416
+ case yDiff < 0 && xDiff <= 0:
28417
+ return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
28418
+ default:
28419
+ return [xMax, yAtXMax];
28420
+ }
28421
+ };
28422
+ const getLines = points => points.map((point, i) => {
28423
+ const next = points[(i + 1) % points.length];
28424
+ return [point, next];
28425
+ });
28426
+
28373
28427
  const defaultStroke = "var(--movable-line-stroke-color)";
28374
28428
  const MovableLine = props => {
28375
28429
  const {
@@ -28380,18 +28434,12 @@ const MovableLine = props => {
28380
28434
  stroke = defaultStroke
28381
28435
  } = props;
28382
28436
  const midpoint = mafs.vec.midpoint(start, end);
28383
- const {
28384
- viewTransform,
28385
- userTransform
28386
- } = mafs.useTransformContext();
28387
- const transformToPx = mafs.vec.matrixMult(viewTransform, userTransform);
28388
- const startPtPx = mafs.vec.transform(start, transformToPx);
28389
- const endPtPx = mafs.vec.transform(end, transformToPx);
28437
+ const [startPtPx, endPtPx] = useTransform(start, end);
28390
28438
  let startExtend = undefined;
28391
28439
  let endExtend = undefined;
28392
28440
  if (extend) {
28393
- startExtend = extend.start ? getExtensionCoords(end, start, extend.range) : undefined;
28394
- endExtend = extend.end ? getExtensionCoords(start, end, extend.range) : undefined;
28441
+ startExtend = extend.start ? getRayIntersectionCoords(end, start, extend.range) : undefined;
28442
+ endExtend = extend.end ? getRayIntersectionCoords(start, end, extend.range) : undefined;
28395
28443
  }
28396
28444
  const line = React.useRef(null);
28397
28445
  const {
@@ -28453,50 +28501,12 @@ function SVGLine(props) {
28453
28501
  });
28454
28502
  }
28455
28503
 
28456
- /**
28457
- * Given two points, find the tips that extends through the points to the edge of the range.
28458
- * @param collinearPoint - The point that the line passes through. Needed to establish slope.
28459
- * @param extendFrom - The point that the line extends from to the edge of the graph.
28460
- */
28461
- const getExtensionCoords = (collinearPoint, extendFrom, range) => {
28462
- // edges of the graph
28463
- const [[xMin, xMax], [yMin, yMax]] = range;
28464
- const [aX, aY] = collinearPoint;
28465
- const [bX, bY] = extendFrom;
28466
- const yDiff = bY - aY;
28467
- const xDiff = bX - aX;
28468
- const slope = yDiff / xDiff;
28469
- const yAtXMin = slope * (xMin - aX) + aY;
28470
- const yAtXMax = slope * (xMax - aX) + aY;
28471
- const xAtYMin = (yMin - aY) / slope + aX;
28472
- const xAtYMax = (yMax - aY) / slope + aX;
28473
-
28474
- // clock analogy to describe quadrants
28475
- switch (true) {
28476
- // 12 o'clock to 2:59
28477
- case yDiff > 0 && xDiff >= 0:
28478
- return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
28479
- // 3 o'clock to 5:59
28480
- case yDiff <= 0 && xDiff > 0:
28481
- // xAtYMin evaluates to -Infinity here, so we use absolute value
28482
- return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
28483
- // 9 o'clock to 11:59
28484
- case yDiff >= 0 && xDiff < 0:
28485
- return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
28486
- // 6 o'clock to 8:59
28487
- case yDiff < 0 && xDiff <= 0:
28488
- return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
28489
- default:
28490
- return [xMax, yAtXMax];
28491
- }
28492
- };
28493
-
28494
28504
  const StyledMovablePoint = props => {
28495
28505
  const hitboxRef = React.useRef(null);
28496
28506
  const {
28497
28507
  point,
28498
28508
  onMove,
28499
- color = Color__default["default"].blue
28509
+ color = wonderBlocksTokens.color.blue
28500
28510
  } = props;
28501
28511
  const {
28502
28512
  dragging
@@ -28506,12 +28516,7 @@ const StyledMovablePoint = props => {
28506
28516
  onMove,
28507
28517
  constrain: p => p
28508
28518
  });
28509
- const {
28510
- viewTransform,
28511
- userTransform
28512
- } = mafs.useTransformContext();
28513
- const transformToPx = mafs.vec.matrixMult(viewTransform, userTransform);
28514
- const [x, y] = mafs.vec.transform(point, transformToPx);
28519
+ const [[x, y]] = useTransform(point);
28515
28520
  return /*#__PURE__*/React__namespace.createElement("g", {
28516
28521
  ref: hitboxRef,
28517
28522
  className: "movable-point",
@@ -28678,6 +28683,175 @@ const RayGraph = props => {
28678
28683
  }));
28679
28684
  };
28680
28685
 
28686
+ /**
28687
+ * Wrapper with Mafs transform styles for SVG elements.
28688
+ * Scales and translates the SVG element to match the Mafs view.
28689
+ * Use if you do not need/want to use the hook in `use-transform.ts`.
28690
+ */
28691
+ const MafsCssTransformWrapper = _ref => {
28692
+ let {
28693
+ children
28694
+ } = _ref;
28695
+ return /*#__PURE__*/React__namespace.createElement("g", {
28696
+ style: {
28697
+ transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28698
+ }
28699
+ }, children);
28700
+ };
28701
+
28702
+ const TextLabel = _ref => {
28703
+ let {
28704
+ children,
28705
+ ...rest
28706
+ } = _ref;
28707
+ return /*#__PURE__*/React__default["default"].createElement(mafs.Text, _extends({
28708
+ size: 16,
28709
+ svgTextProps: {
28710
+ filter: "url(#background)",
28711
+ fontWeight: "bold"
28712
+ }
28713
+ }, rest), children);
28714
+ };
28715
+ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs", null, /*#__PURE__*/React__default["default"].createElement("filter", {
28716
+ id: "background",
28717
+ x: "-5%",
28718
+ width: "110%",
28719
+ y: "0%",
28720
+ height: "100%"
28721
+ }, /*#__PURE__*/React__default["default"].createElement("feFlood", {
28722
+ floodColor: "#FFF",
28723
+ floodOpacity: "0.64"
28724
+ }), /*#__PURE__*/React__default["default"].createElement("feComposite", {
28725
+ operator: "over",
28726
+ in: "SourceGraphic"
28727
+ })));
28728
+
28729
+ const Angle = _ref => {
28730
+ let {
28731
+ centerPoint,
28732
+ endPoints,
28733
+ range,
28734
+ polygonLines,
28735
+ showAngles
28736
+ } = _ref;
28737
+ const [centerX, centerY] = centerPoint;
28738
+ const areClockwise = clockwise$1([centerPoint, ...endPoints]);
28739
+ const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
28740
+ const radius = 0.3;
28741
+ const a = mafs.vec.dist(centerPoint, endPoints[0]);
28742
+ const b = mafs.vec.dist(centerPoint, endPoints[1]);
28743
+ const c = mafs.vec.dist(endPoints[0], endPoints[1]);
28744
+
28745
+ // Law of cosines
28746
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
28747
+ const y1 = centerY + (startY - centerY) / a * radius;
28748
+ const x2 = centerX + (endX - centerX) / b * radius;
28749
+ const x1 = centerX + (startX - centerX) / a * radius;
28750
+ const y2 = centerY + (endY - centerY) / b * radius;
28751
+
28752
+ // Fourth point that would create a rhomboid
28753
+ // Used to calculate the inside of the angle and to create square
28754
+ const [x3, y3] = mafs.vec.add(centerPoint, mafs.vec.add(mafs.vec.sub([x1, y1], centerPoint), mafs.vec.sub([x2, y2], centerPoint)));
28755
+ if (!showAngles) {
28756
+ return isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
28757
+ start: [x1, y1],
28758
+ vertex: [x2, y2],
28759
+ end: [x3, y3]
28760
+ }) : null;
28761
+ }
28762
+
28763
+ // Midpoint betwen ends of arc
28764
+ const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
28765
+ const largeArcFlag = isOutside ? 1 : 0;
28766
+ const sweepFlag = isOutside ? 1 : 0;
28767
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
28768
+ let angleInDegrees = angle * (180 / Math.PI);
28769
+ // If we have triggered "largArcFlag", the angle should be greater than 180
28770
+ if (isOutside) {
28771
+ angleInDegrees = 360 - angleInDegrees;
28772
+ }
28773
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
28774
+ const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
28775
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
28776
+ id: "background",
28777
+ x: "-5%",
28778
+ width: "110%",
28779
+ y: "0%",
28780
+ height: "100%"
28781
+ }, /*#__PURE__*/React__namespace.createElement("feFlood", {
28782
+ floodColor: "#FFF",
28783
+ floodOpacity: "0.5"
28784
+ }), /*#__PURE__*/React__namespace.createElement("feComposite", {
28785
+ operator: "over",
28786
+ in: "SourceGraphic"
28787
+ }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
28788
+ start: [x1, y1],
28789
+ vertex: [x2, y2],
28790
+ end: [x3, y3]
28791
+ }) : /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
28792
+ d: arc,
28793
+ strokeWidth: 0.02,
28794
+ fill: "none"
28795
+ })), /*#__PURE__*/React__namespace.createElement(TextLabel, {
28796
+ x: x3,
28797
+ y: y3,
28798
+ attach: y3 - centerY > 0 ? "s" : "n",
28799
+ attachDistance: Math.abs(y3 - centerY) < 0.2 || mafs.vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
28800
+ }, angleLabel, "\xB0"));
28801
+ };
28802
+
28803
+ /**
28804
+ * This is broken out into its own component so that it can be used for an early return
28805
+ * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
28806
+ */
28807
+ const RightAngleSquare = _ref2 => {
28808
+ let {
28809
+ start: [x1, y1],
28810
+ vertex: [x2, y2],
28811
+ end: [x3, y3]
28812
+ } = _ref2;
28813
+ return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
28814
+ d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
28815
+ strokeWidth: 0.02,
28816
+ fill: "none"
28817
+ }));
28818
+ };
28819
+ const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
28820
+
28821
+ /**
28822
+ * Determines if an angle is an inside (false) or outside (true) angle.
28823
+ * They way, we know to flip the `largeArc` and `sweepArc` flags.
28824
+ * Uses the priciple that a ray from a point inside a polygon will intersect
28825
+ * with an odd number of lines, while a ray from a point outside the polygon
28826
+ * will intersect with an even number of lines.
28827
+ * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
28828
+ */
28829
+ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
28830
+ // Create a ray from the midpoint (inside angle) to the edge of the range
28831
+ const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
28832
+ let lineIntersectionCount = 0;
28833
+ polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
28834
+
28835
+ // If the number of intersections is even, the angle is inside the polygon
28836
+ return !isEven(lineIntersectionCount);
28837
+ };
28838
+
28839
+ // https://stackoverflow.com/a/24392281/7347484
28840
+ // The "intersects" function in geometry doesn't seem to work for this use case
28841
+ const linesIntersect = (_ref3, _ref4) => {
28842
+ let [[a, b], [c, d]] = _ref3;
28843
+ let [[p, q], [r, s]] = _ref4;
28844
+ const determinant = (c - a) * (s - q) - (r - p) * (d - b);
28845
+ if (determinant === 0) {
28846
+ return false;
28847
+ } else {
28848
+ const lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / determinant;
28849
+ const gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / determinant;
28850
+ return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1;
28851
+ }
28852
+ };
28853
+ const isEven = n => n % 2 === 0;
28854
+
28681
28855
  const PolygonGraph = props => {
28682
28856
  const [focused, setFocused] = React__namespace.useState(false);
28683
28857
  const [hovered, setHovered] = React__namespace.useState(false);
@@ -28686,7 +28860,10 @@ const PolygonGraph = props => {
28686
28860
  } = props;
28687
28861
  const {
28688
28862
  coords,
28689
- type
28863
+ type,
28864
+ showAngles,
28865
+ showSides,
28866
+ range
28690
28867
  } = props.graphState;
28691
28868
  const points = coords ?? [[0, 0]];
28692
28869
  const pointsSum = points.reduce((acc, point) => mafs.vec.add(acc, point), [0, 0]);
@@ -28704,12 +28881,37 @@ const PolygonGraph = props => {
28704
28881
  constrain: p => p
28705
28882
  });
28706
28883
  const active = hovered || focused || dragging;
28884
+ const lines = getLines(points);
28707
28885
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28708
28886
  points: [...points],
28709
28887
  color: "var(--movable-line-stroke-color)",
28710
28888
  svgPolygonProps: {
28711
28889
  strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)"
28712
28890
  }
28891
+ }), points.map((point, i) => {
28892
+ const pt1 = points.at(i - 1);
28893
+ const pt2 = points[(i + 1) % points.length];
28894
+ if (!pt1 || !pt2) {
28895
+ return null;
28896
+ }
28897
+ return /*#__PURE__*/React__namespace.createElement(Angle, {
28898
+ key: "angle-" + i,
28899
+ centerPoint: point,
28900
+ endPoints: [pt1, pt2],
28901
+ active: active,
28902
+ range: range,
28903
+ polygonLines: lines,
28904
+ showAngles: !!showAngles
28905
+ });
28906
+ }), showSides && lines.map((_ref, i) => {
28907
+ let [start, end] = _ref;
28908
+ const [x, y] = mafs.vec.midpoint(start, end);
28909
+ const length = parseFloat(mafs.vec.dist(start, end).toFixed(1));
28910
+ return /*#__PURE__*/React__namespace.createElement(TextLabel, {
28911
+ key: "side-" + i,
28912
+ x: x,
28913
+ y: y
28914
+ }, !Number.isInteger(length) && "≈ ", length);
28713
28915
  }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
28714
28916
  points: [...points],
28715
28917
  color: "transparent",
@@ -28727,7 +28929,7 @@ const PolygonGraph = props => {
28727
28929
  className: "movable-polygon"
28728
28930
  }
28729
28931
  }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
28730
- key: i,
28932
+ key: "point-" + i,
28731
28933
  point: point,
28732
28934
  onMove: destination => dispatch(moveControlPoint(i, destination)),
28733
28935
  "data-testid": type + i
@@ -29409,7 +29611,11 @@ var components = /*#__PURE__*/Object.freeze({
29409
29611
  * If a graphie URL is provided in `backgroundImage`, will return the rendered graphie background.
29410
29612
  * Otherwise, returns `null`.
29411
29613
  */
29412
- const getLegacyGrid = (box, backgroundImage) => {
29614
+ const LegacyGrid = _ref => {
29615
+ let {
29616
+ box,
29617
+ backgroundImage
29618
+ } = _ref;
29413
29619
  const {
29414
29620
  url,
29415
29621
  width,
@@ -29423,10 +29629,10 @@ const getLegacyGrid = (box, backgroundImage) => {
29423
29629
  bottom: 0,
29424
29630
  left: 0
29425
29631
  }
29426
- }, /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref => {
29632
+ }, /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref2 => {
29427
29633
  let {
29428
29634
  setAssetStatus
29429
- } = _ref;
29635
+ } = _ref2;
29430
29636
  return /*#__PURE__*/React__namespace.createElement(SvgImage, {
29431
29637
  src: url,
29432
29638
  width: width,
@@ -29698,10 +29904,10 @@ function initializeGraphState(params) {
29698
29904
  };
29699
29905
  case "polygon":
29700
29906
  return {
29701
- type: "polygon",
29702
29907
  hasBeenInteractedWith: false,
29703
29908
  range,
29704
29909
  snapStep,
29910
+ ...graph,
29705
29911
  coords: getPolygonCoords({
29706
29912
  graph,
29707
29913
  range,
@@ -29723,31 +29929,41 @@ function initializeGraphState(params) {
29723
29929
  // Instead of updating to v5, using generic type to relate input and output types.
29724
29930
  function getGradableGraph(state, initialGraph) {
29725
29931
  if (!state.hasBeenInteractedWith) {
29726
- return initialGraph;
29932
+ return {
29933
+ type: initialGraph.type
29934
+ };
29727
29935
  }
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);
29936
+ if (initialGraph.type === "linear-system" && state.type === "linear-system") {
29937
+ return {
29938
+ ...initialGraph,
29939
+ coords: state.coords
29940
+ };
29750
29941
  }
29942
+ if (state.type === "segment" && initialGraph.type === "segment") {
29943
+ return {
29944
+ ...initialGraph,
29945
+ coords: state.coords
29946
+ };
29947
+ }
29948
+ if (state.type === "linear" && initialGraph.type === "linear") {
29949
+ return {
29950
+ ...initialGraph,
29951
+ coords: state.coords?.[0]
29952
+ };
29953
+ }
29954
+ if (state.type === "ray" && initialGraph.type === "ray") {
29955
+ return {
29956
+ ...initialGraph,
29957
+ coords: state.coords?.[0]
29958
+ };
29959
+ }
29960
+ if (state.type === "polygon" && initialGraph.type === "polygon") {
29961
+ return {
29962
+ ...initialGraph,
29963
+ coords: state.coords
29964
+ };
29965
+ }
29966
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29751
29967
  }
29752
29968
  const getDefaultSegments = _ref => {
29753
29969
  let {
@@ -29771,9 +29987,10 @@ const getDefaultSegments = _ref => {
29771
29987
  return [5];
29772
29988
  }
29773
29989
  };
29990
+ const defaultRange = [[-10, 10], [-10, 10]];
29774
29991
  return ys(graph.numSegments).map(y => {
29775
29992
  let endpoints = [[-5, y], [5, y]];
29776
- endpoints = normalizeCoords(endpoints, range);
29993
+ endpoints = normalizeCoords(endpoints, defaultRange);
29777
29994
  endpoints = normalizePoints(range, step, endpoints);
29778
29995
  return endpoints;
29779
29996
  });
@@ -29856,7 +30073,6 @@ const renderGraph = props => {
29856
30073
  };
29857
30074
  const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
29858
30075
  const [width, height] = props.box;
29859
- const legacyGrid = getLegacyGrid([width, height], props.backgroundImage);
29860
30076
  const [state, dispatch] = React__namespace.useReducer(interactiveGraphReducer, props, initializeGraphState);
29861
30077
  React__namespace.useImperativeHandle(ref, () => ({
29862
30078
  getUserInput: () => getGradableGraph(state, props.graph)
@@ -29867,13 +30083,17 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
29867
30083
  height,
29868
30084
  position: "relative"
29869
30085
  }
29870
- }, legacyGrid, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
30086
+ }, /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
30087
+ box: props.box,
30088
+ backgroundImage: props.backgroundImage
30089
+ }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
29871
30090
  style: {
29872
30091
  position: "absolute",
29873
30092
  bottom: 0,
29874
30093
  left: 0
29875
30094
  }
29876
30095
  }, /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
30096
+ preserveAspectRatio: false,
29877
30097
  viewBox: {
29878
30098
  x: props.range[0],
29879
30099
  y: props.range[1],
@@ -29883,7 +30103,7 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
29883
30103
  zoom: false,
29884
30104
  width: width,
29885
30105
  height: height
29886
- }, !legacyGrid && /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30106
+ }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
29887
30107
  lockedFigures: props.lockedFigures
29888
30108
  }), renderGraph({
29889
30109
  state,
@@ -31509,18 +31729,18 @@ class InteractiveGraph extends React__namespace.Component {
31509
31729
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
31510
31730
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
31511
31731
  }
31512
- static validate(
31513
- // TODO(kevinb): rename state to userInput
31514
- // state === userInput
31515
- state, rubric, component) {
31732
+ static validate(userInput, rubric, component) {
31516
31733
  // When nothing has moved, there will neither be coords nor the
31517
31734
  // circle's center/radius fields. When those fields are absent, skip
31518
31735
  // all these checks; just go mark the answer as empty.
31519
- // @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'.
31520
- const hasValue = !!(state.coords || state.center && state.radius);
31521
- if (state.type === rubric.correct.type && hasValue) {
31522
- if (state.type === "linear" && rubric.correct.type === "linear" && state.coords != null) {
31523
- const guess = state.coords;
31736
+ const hasValue = Boolean(
31737
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
31738
+ userInput.coords ||
31739
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
31740
+ userInput.center && userInput.radius);
31741
+ if (userInput.type === rubric.correct.type && hasValue) {
31742
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
31743
+ const guess = userInput.coords;
31524
31744
  const correct = rubric.correct.coords;
31525
31745
 
31526
31746
  // If both of the guess points are on the correct line, it's
@@ -31537,8 +31757,8 @@ class InteractiveGraph extends React__namespace.Component {
31537
31757
  message: null
31538
31758
  };
31539
31759
  }
31540
- } else if (state.type === "linear-system" && rubric.correct.type === "linear-system" && state.coords != null) {
31541
- const guess = state.coords;
31760
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
31761
+ const guess = userInput.coords;
31542
31762
  const correct = rubric.correct.coords;
31543
31763
  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])) {
31544
31764
  return {
@@ -31548,9 +31768,9 @@ class InteractiveGraph extends React__namespace.Component {
31548
31768
  message: null
31549
31769
  };
31550
31770
  }
31551
- } else if (state.type === "quadratic" && rubric.correct.type === "quadratic" && state.coords != null) {
31771
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
31552
31772
  // If the parabola coefficients match, it's correct.
31553
- const guessCoeffs = this.getQuadraticCoefficients(state.coords);
31773
+ const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
31554
31774
  const correctCoeffs = this.getQuadraticCoefficients(
31555
31775
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31556
31776
  rubric.correct.coords);
@@ -31562,8 +31782,8 @@ class InteractiveGraph extends React__namespace.Component {
31562
31782
  message: null
31563
31783
  };
31564
31784
  }
31565
- } else if (state.type === "sinusoid" && rubric.correct.type === "sinusoid" && state.coords != null) {
31566
- const guessCoeffs = this.getSinusoidCoefficients(state.coords);
31785
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
31786
+ const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
31567
31787
  const correctCoeffs = this.getSinusoidCoefficients(
31568
31788
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31569
31789
  rubric.correct.coords);
@@ -31578,8 +31798,8 @@ class InteractiveGraph extends React__namespace.Component {
31578
31798
  message: null
31579
31799
  };
31580
31800
  }
31581
- } else if (state.type === "circle" && rubric.correct.type === "circle") {
31582
- if (deepEq$1(state.center, rubric.correct.center) && eq(state.radius, rubric.correct.radius)) {
31801
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
31802
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
31583
31803
  return {
31584
31804
  type: "points",
31585
31805
  earned: 1,
@@ -31587,9 +31807,9 @@ class InteractiveGraph extends React__namespace.Component {
31587
31807
  message: null
31588
31808
  };
31589
31809
  }
31590
- } else if (state.type === "point" && rubric.correct.type === "point" && state.coords != null) {
31810
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
31591
31811
  let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
31592
- const guess = state.coords?.slice();
31812
+ const guess = userInput.coords?.slice();
31593
31813
  correct = correct.slice();
31594
31814
  // Everything's already rounded so we shouldn't need to do an
31595
31815
  // eq() comparison but _.isEqual(0, -0) is false, so we'll use
@@ -31606,8 +31826,8 @@ class InteractiveGraph extends React__namespace.Component {
31606
31826
  message: null
31607
31827
  };
31608
31828
  }
31609
- } else if (state.type === "polygon" && rubric.correct.type === "polygon" && state.coords != null) {
31610
- const guess = state.coords?.slice();
31829
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
31830
+ const guess = userInput.coords?.slice();
31611
31831
  // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
31612
31832
  const correct = rubric.correct.coords?.slice();
31613
31833
  let match;
@@ -31631,8 +31851,8 @@ class InteractiveGraph extends React__namespace.Component {
31631
31851
  message: null
31632
31852
  };
31633
31853
  }
31634
- } else if (state.type === "segment" && rubric.correct.type === "segment" && state.coords != null) {
31635
- let guess = state.coords.slice();
31854
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
31855
+ let guess = userInput.coords.slice();
31636
31856
  let correct = rubric.correct.coords?.slice();
31637
31857
  guess = ___default["default"].invoke(guess, "sort").sort();
31638
31858
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
@@ -31645,8 +31865,8 @@ class InteractiveGraph extends React__namespace.Component {
31645
31865
  message: null
31646
31866
  };
31647
31867
  }
31648
- } else if (state.type === "ray" && rubric.correct.type === "ray" && state.coords != null) {
31649
- const guess = state.coords;
31868
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
31869
+ const guess = userInput.coords;
31650
31870
  const correct = rubric.correct.coords;
31651
31871
  if (
31652
31872
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
@@ -31660,8 +31880,8 @@ class InteractiveGraph extends React__namespace.Component {
31660
31880
  message: null
31661
31881
  };
31662
31882
  }
31663
- } else if (state.type === "angle" && rubric.correct.type === "angle") {
31664
- const guess = state.coords;
31883
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
31884
+ const guess = userInput.coords;
31665
31885
  const correct = rubric.correct.coords;
31666
31886
  let match;
31667
31887
  if (rubric.correct.match === "congruent") {
@@ -31700,7 +31920,7 @@ class InteractiveGraph extends React__namespace.Component {
31700
31920
 
31701
31921
  // The input wasn't correct, so check if it's a blank input or if it's
31702
31922
  // actually just wrong
31703
- if (!hasValue || ___default["default"].isEqual(state, rubric.graph)) {
31923
+ if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
31704
31924
  // We're where we started.
31705
31925
  return {
31706
31926
  type: "invalid",
@@ -32777,7 +32997,7 @@ const styles$g = aphrodite.StyleSheet.create({
32777
32997
  });
32778
32998
 
32779
32999
  const BringToFront = {
32780
- boxShadow: `0 8px 8px ${Color__default["default"].offBlack64}`,
33000
+ boxShadow: `0 8px 8px ${wonderBlocksTokens.color.offBlack64}`,
32781
33001
  zIndex: 1000
32782
33002
  };
32783
33003
  const AnswerPill = props => {
@@ -32833,7 +33053,7 @@ const styles$f = aphrodite.StyleSheet.create({
32833
33053
  backgroundColor: "#00880b"
32834
33054
  },
32835
33055
  incorrect: {
32836
- backgroundColor: Color__default["default"].offBlack64
33056
+ backgroundColor: wonderBlocksTokens.color.offBlack64
32837
33057
  },
32838
33058
  pill: {
32839
33059
  // Reset the Pill's default height in order to account
@@ -32877,7 +33097,7 @@ class Marker extends React__namespace.Component {
32877
33097
  // default dot
32878
33098
  let args = {
32879
33099
  size: MARKER_SIZE,
32880
- color: Color__default["default"].white,
33100
+ color: wonderBlocksTokens.color.white,
32881
33101
  icon: iconNull
32882
33102
  };
32883
33103
  if (showCorrectness) {
@@ -32938,7 +33158,7 @@ class Marker extends React__namespace.Component {
32938
33158
  const styles$e = aphrodite.StyleSheet.create({
32939
33159
  marker: {
32940
33160
  position: "absolute",
32941
- backgroundColor: Color__default["default"].white,
33161
+ backgroundColor: wonderBlocksTokens.color.white,
32942
33162
  borderRadius: MARKER_SIZE,
32943
33163
  // Center marker position based on its maximum size.
32944
33164
  width: MARKER_SIZE,
@@ -32946,7 +33166,7 @@ const styles$e = aphrodite.StyleSheet.create({
32946
33166
  marginLeft: MARKER_SIZE / -2,
32947
33167
  marginTop: MARKER_SIZE / -2,
32948
33168
  // Add a shadow to the marker to make it stand out from the image.
32949
- boxShadow: `0 8px 8px ${Color__default["default"].offBlack8}`
33169
+ boxShadow: `0 8px 8px ${wonderBlocksTokens.color.offBlack8}`
32950
33170
  },
32951
33171
  // The base and unfilled marker style.
32952
33172
  markerIcon: {
@@ -32956,7 +33176,7 @@ const styles$e = aphrodite.StyleSheet.create({
32956
33176
  boxSizing: "border-box",
32957
33177
  width: MARKER_SIZE,
32958
33178
  height: MARKER_SIZE,
32959
- border: `2px solid ${Color__default["default"].offBlack64}`,
33179
+ border: `2px solid ${wonderBlocksTokens.color.offBlack64}`,
32960
33180
  borderRadius: MARKER_SIZE
32961
33181
  },
32962
33182
  // The animation that presents the marker to the learner
@@ -32964,11 +33184,11 @@ const styles$e = aphrodite.StyleSheet.create({
32964
33184
  animationName: {
32965
33185
  "0%": {
32966
33186
  transform: "scale(1)",
32967
- backgroundColor: Color__default["default"].blue
33187
+ backgroundColor: wonderBlocksTokens.color.blue
32968
33188
  },
32969
33189
  "100%": {
32970
33190
  transform: "scale(1.3)",
32971
- backgroundColor: Color__default["default"].blue
33191
+ backgroundColor: wonderBlocksTokens.color.blue
32972
33192
  }
32973
33193
  },
32974
33194
  animationDirection: "alternate",
@@ -32985,35 +33205,35 @@ const styles$e = aphrodite.StyleSheet.create({
32985
33205
  animationIterationCount: "2"
32986
33206
  },
32987
33207
  markerActive: {
32988
- outline: `2px solid ${Color__default["default"].blue}`,
33208
+ outline: `2px solid ${wonderBlocksTokens.color.blue}`,
32989
33209
  outlineOffset: 2
32990
33210
  },
32991
33211
  // The learner is making an initial selection
32992
33212
  markerSelected: {
32993
- boxShadow: `0 8px 8px ${Color__default["default"].offBlack8}`,
32994
- border: `solid 4px ${Color__default["default"].white}`,
32995
- backgroundColor: Color__default["default"].blue,
33213
+ boxShadow: `0 8px 8px ${wonderBlocksTokens.color.offBlack8}`,
33214
+ border: `solid 4px ${wonderBlocksTokens.color.white}`,
33215
+ backgroundColor: wonderBlocksTokens.color.blue,
32996
33216
  borderRadius: MARKER_SIZE,
32997
33217
  transform: "rotate(180deg)"
32998
33218
  },
32999
33219
  // The learner has made a selection
33000
33220
  markerFilled: {
33001
33221
  backgroundColor: "#ECF3FE",
33002
- border: `4px solid ${Color__default["default"].blue}`
33222
+ border: `4px solid ${wonderBlocksTokens.color.blue}`
33003
33223
  },
33004
33224
  markerGraded: {
33005
33225
  width: MARKER_SIZE,
33006
33226
  height: MARKER_SIZE,
33007
33227
  justifyContent: "center",
33008
33228
  alignItems: "center",
33009
- border: `2px solid ${Color__default["default"].white}`
33229
+ border: `2px solid ${wonderBlocksTokens.color.white}`
33010
33230
  },
33011
33231
  markerCorrect: {
33012
33232
  background: "#00880b" // WB green darkened by 18%
33013
33233
  },
33014
33234
 
33015
33235
  markerIncorrect: {
33016
- background: Color__default["default"].offBlack64
33236
+ background: wonderBlocksTokens.color.offBlack64
33017
33237
  }
33018
33238
  });
33019
33239
 
@@ -33444,6 +33664,7 @@ class LabelImage extends React__namespace.Component {
33444
33664
  const answerChoicesActive = index === activeMarkerIndex;
33445
33665
  const showAnswerChoice = marker.selected && !answerChoicesActive && !this.state.hideAnswers;
33446
33666
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
33667
+ key: index,
33447
33668
  style: {
33448
33669
  position: "absolute",
33449
33670
  left: `${marker.x}%`,
@@ -38048,7 +38269,7 @@ const styles$8 = aphrodite.StyleSheet.create({
38048
38269
  // and the "Add highlight" action fails.
38049
38270
  userSelect: "none",
38050
38271
  fontFamily: `"Lato", sans-serif`,
38051
- padding: `10px ${Spacing__default["default"].medium_16}px`,
38272
+ padding: `10px ${wonderBlocksTokens.spacing.medium_16}px`,
38052
38273
  cursor: "pointer"
38053
38274
  }
38054
38275
  });
@@ -41894,7 +42115,7 @@ const init = function (options) {
41894
42115
 
41895
42116
  // This file is processed by a Rollup plugin (replace) to inject the production
41896
42117
  const libName = "@khanacademy/perseus";
41897
- const libVersion = "20.8.0";
42118
+ const libVersion = "21.0.0";
41898
42119
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
41899
42120
 
41900
42121
  const apiVersion = {
@@ -44143,6 +44364,138 @@ const registerAllWidgetsForTesting = () => {
44143
44364
  replaceDeprecatedWidgets();
44144
44365
  };
44145
44366
 
44367
+ // These are all the current widgets that require user-input and are supported for automatic scoring
44368
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44369
+
44370
+ /**
44371
+ * Regex for widget placeholders in a string.
44372
+ *
44373
+ * First capture group is the widget ID (ex. 'radio 1')
44374
+ * Second capture group is the widget type (ex. "radio)
44375
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44376
+ */
44377
+ function getWidgetRegex() {
44378
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
44379
+ }
44380
+
44381
+ /**
44382
+ * Add a widget placeholder using the widget ID.
44383
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
44384
+ *
44385
+ * @param {string} id
44386
+ * @returns {string}
44387
+ */
44388
+ function addWidget(id) {
44389
+ return `[[☃ ${id}]]`;
44390
+ }
44391
+
44392
+ /**
44393
+ * Get a widget type by a widget's ID
44394
+ * (please don't derive type from ID)
44395
+ *
44396
+ * @param {string} widgetId the ID of the widget
44397
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44398
+ * @returns {string} the widget type (ie "radio")
44399
+ */
44400
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44401
+ const widget = widgetMap[WidgetId];
44402
+ return widget?.type ?? null;
44403
+ }
44404
+
44405
+ /**
44406
+ * Does the content have a specific type of widget?
44407
+ *
44408
+ * @param {string} type the type of the widget in question (ie "radio")
44409
+ * @param {string} content the string to search through
44410
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44411
+ * @returns {boolean} if the content includes the widget type
44412
+ */
44413
+ function contentHasWidgetType(type, content, widgetMap) {
44414
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
44415
+ }
44416
+
44417
+ /**
44418
+ * Extract all widget IDs, which includes the widget type and instance number.
44419
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44420
+ *
44421
+ * Content should contain Perseus widget placeholders,
44422
+ * which look like: '[[☃ radio 1]]'.
44423
+ *
44424
+ * @param {string} content
44425
+ * @returns {ReadonlyArray<string>} widgetIds
44426
+ */
44427
+ function getWidgetIdsFromContent(content) {
44428
+ const widgets = [];
44429
+ const localWidgetRegex = getWidgetRegex();
44430
+ let match = localWidgetRegex.exec(content);
44431
+ while (match !== null) {
44432
+ widgets.push(match[1]);
44433
+ match = localWidgetRegex.exec(content);
44434
+ }
44435
+ return widgets;
44436
+ }
44437
+
44438
+ /**
44439
+ * Get a list of widget IDs from content,
44440
+ * but only for specific widget types
44441
+ *
44442
+ * @param {string} type the type of widget (ie "radio")
44443
+ * @param {string} content the string to parse
44444
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44445
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
44446
+ */
44447
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
44448
+ const rv = [];
44449
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
44450
+ widgetIdsInContent.forEach(widgetId => {
44451
+ const widget = widgetMap[widgetId];
44452
+ if (widget?.type === type) {
44453
+ rv.push(widgetId);
44454
+ }
44455
+ });
44456
+ return rv;
44457
+ }
44458
+
44459
+ /**
44460
+ * Pull the widget map out of ItemData
44461
+ *
44462
+ * @param {PerseusItem} PerseusItem containing a widgetMap
44463
+ * @returns {WidgetMap} the widget map in the PerseusItem
44464
+ */
44465
+ function getWidgetsMapFromItemData(itemData) {
44466
+ return itemData.question.widgets;
44467
+ }
44468
+
44469
+ /**
44470
+ * Get the widget information from a WidgetMap
44471
+ *
44472
+ * @param {string} widgetId the ID of the widget
44473
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
44474
+ */
44475
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
44476
+ return widgetMap[widgetId] ?? null;
44477
+ }
44478
+
44479
+ /**
44480
+ * Get select widgets from a widget map.
44481
+ * Useful for multi-items that needs to split
44482
+ * one PerseusItem into several
44483
+ *
44484
+ * @param {ReadonlyArray<string>} widgetIds to extract
44485
+ * @param {PerseusWidgetsMap} widgetMap to extract from
44486
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
44487
+ */
44488
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
44489
+ const widgets = {};
44490
+ widgetIds.forEach(widgetId => {
44491
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
44492
+ if (widget) {
44493
+ widgets[widgetId] = widget;
44494
+ }
44495
+ });
44496
+ return widgets;
44497
+ }
44498
+
44146
44499
  /**
44147
44500
  * This function extracts the answers from the widgets.
44148
44501
  *
@@ -44547,18 +44900,15 @@ const INDIVIDUAL_ANSWER_WIDGETS = ["interactive-graph", "categorizer", "grapher"
44547
44900
 
44548
44901
  /* Widgets that are supported for automatic scoring */
44549
44902
  const SUPPORTED_WIDGETS = ["radio", "numeric-input", "input-number", "expression", ...INDIVIDUAL_ANSWER_WIDGETS];
44550
- const getWidgetTypeFromWidgetKey = widgetKey => {
44551
- return widgetKey.split(" ")[0];
44552
- };
44553
44903
 
44554
44904
  /* 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)));
44905
+ const isWrongAnswerSupported = (widgetIds, widgetMap) => {
44906
+ return widgetIds.length !== 0 && widgetIds.every(widgetId => SUPPORTED_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap)));
44557
44907
  };
44558
44908
 
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));
44909
+ /* Verify if the widget ID has an individual answer for the coach report view */
44910
+ const shouldHaveIndividualAnswer = (widgetId, widgetMap) => {
44911
+ return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap));
44562
44912
  };
44563
44913
 
44564
44914
  /* Returns the answer userInput submission for currently supported widgets */
@@ -44577,29 +44927,30 @@ const getAnswerFromUserInput = (widgetType, userInput) => {
44577
44927
  return userInput;
44578
44928
  };
44579
44929
 
44580
- /* Returns the correct answer for a given widget key and Perseus Item */
44930
+ /* Returns the correct answer for a given widget ID and Perseus Item */
44581
44931
  // TODO (LEMS-1835): We should fix the resonse type from getWidget to be specific.
44582
44932
  // 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);
44933
+ const getCorrectAnswerForWidgetId = (widgetId, itemData) => {
44934
+ const rubric = itemData.question.widgets[widgetId].options;
44935
+ const widgetMap = getWidgetsMapFromItemData(itemData);
44936
+ const widgetType = getWidgetTypeByWidgetId(widgetId, widgetMap);
44586
44937
  const widget = getWidget(widgetType);
44587
44938
 
44588
44939
  // @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType<any>'.
44589
44940
  return widget?.getOneCorrectAnswerFromRubric?.(rubric);
44590
44941
  };
44591
44942
 
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;
44943
+ /* Verify if the widget ID exists in the content string of the Perseus Item */
44944
+ const isWidgetIdInContent = (perseusItem, widgetId) => {
44945
+ return perseusItem.question.content.indexOf(widgetId) !== -1;
44595
44946
  };
44596
44947
 
44597
- /* Return an array of all the widget keys that exist in the content string of a Perseus Item */
44598
- const getValidWidgetKeys = perseusItem => {
44948
+ /* Return an array of all the widget IDs that exist in the content string of a Perseus Item */
44949
+ const getValidWidgetIds = perseusItem => {
44599
44950
  const {
44600
44951
  widgets
44601
44952
  } = perseusItem.question;
44602
- return wonderStuffCore.keys(widgets).filter(key => isWidgetKeyInContent(perseusItem, key));
44953
+ return wonderStuffCore.keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
44603
44954
  };
44604
44955
 
44605
44956
  /**
@@ -44647,7 +44998,7 @@ const genericPerseusItemData = {
44647
44998
  * uses a basic Perseus object and updates it with custom values as needed.
44648
44999
  *
44649
45000
  * @param {Partial<PerseusItem>} customFields
44650
- * @returns PerseusItem
45001
+ * @returns {PerseusItem}
44651
45002
  */
44652
45003
  function generateTestPerseusItem() {
44653
45004
  let customFields = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -44657,6 +45008,107 @@ function generateTestPerseusItem() {
44657
45008
  };
44658
45009
  }
44659
45010
 
45011
+ /**
45012
+ * Creates an object with the bare amount
45013
+ * of data to be a properly typed RadioWidget
45014
+ *
45015
+ * @returns {RadioWidget} skeleton RadioWidget for testing
45016
+ */
45017
+ function generateTestRadioWidget() {
45018
+ return {
45019
+ type: "radio",
45020
+ options: {
45021
+ choices: []
45022
+ }
45023
+ };
45024
+ }
45025
+
45026
+ /**
45027
+ * Creates an object with the bare amount
45028
+ * of data to be a properly typed InteractiveGraphWidget
45029
+ *
45030
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
45031
+ */
45032
+ function generateTestInteractiveGraphWidget() {
45033
+ return {
45034
+ type: "interactive-graph",
45035
+ options: {
45036
+ step: [1, 1],
45037
+ gridStep: [1, 1],
45038
+ snapStep: [1, 1],
45039
+ markings: "graph",
45040
+ labels: ["x", "y"],
45041
+ showProtractor: false,
45042
+ showRuler: false,
45043
+ rulerLabel: "mm",
45044
+ rulerTicks: 2,
45045
+ range: [[-10, 10], [-10, 10]],
45046
+ graph: {
45047
+ type: "angle"
45048
+ },
45049
+ correct: {
45050
+ type: "angle"
45051
+ }
45052
+ }
45053
+ };
45054
+ }
45055
+
45056
+ /**
45057
+ * Creates an object with the bare amount
45058
+ * of data to be a properly typed InteractiveGraphWidget
45059
+ *
45060
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
45061
+ */
45062
+ function generateTestCategorizerWidget() {
45063
+ return {
45064
+ type: "categorizer",
45065
+ options: {
45066
+ items: [],
45067
+ categories: [],
45068
+ randomizeItems: false,
45069
+ static: false,
45070
+ values: []
45071
+ }
45072
+ };
45073
+ }
45074
+
45075
+ /**
45076
+ * Creates an object with the bare amount
45077
+ * of data to be a properly typed ExpressionWidget
45078
+ *
45079
+ * @returns {ExpressionWidget} skeleton ExpressionWidget for testing
45080
+ */
45081
+ function generateTestExpressionWidget() {
45082
+ return {
45083
+ type: "expression",
45084
+ options: {
45085
+ answerForms: [],
45086
+ buttonSets: ["basic"],
45087
+ functions: [],
45088
+ times: false
45089
+ }
45090
+ };
45091
+ }
45092
+
45093
+ /**
45094
+ * Creates an object with the bare amount
45095
+ * of data to be a properly typed NumericInputWidget
45096
+ *
45097
+ * @returns {NumericInputWidget} skeleton NumericInputWidget for testing
45098
+ */
45099
+ function generateTestNumericInputWidget() {
45100
+ return {
45101
+ type: "numeric-input",
45102
+ options: {
45103
+ answers: [],
45104
+ labelText: "",
45105
+ size: "normal",
45106
+ coefficient: false,
45107
+ static: false
45108
+ }
45109
+ };
45110
+ }
45111
+
44660
45112
  /**
44661
45113
  * Some helpers for CEDAR logging. CEDAR convention dictates
44662
45114
  * that enum values should be upper case. Ex: for the
@@ -44711,163 +45163,6 @@ function convertWidgetNameToEnum(name) {
44711
45163
  }
44712
45164
  return widgetEnum;
44713
45165
  }
44714
- function getWidgetTypeFromWidgetId(widgetId) {
44715
- const [widgetType, widgetNumber] = widgetId.split(" ");
44716
- if (!widgetNumber) {
44717
- throw new Error(`Incorrect format for widget ID: ${widgetId}`);
44718
- }
44719
- if (!widgetNameToEnum[widgetType]) {
44720
- throw new Error(`Unknown widget type (${widgetType}) in widget ID: ${widgetId}`);
44721
- }
44722
- return widgetType;
44723
- }
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
45166
 
44872
45167
  const EditorJsonify = {
44873
45168
  serialize: function () {
@@ -44919,7 +45214,6 @@ exports.TableWidget = Table$1;
44919
45214
  exports.Util = Util;
44920
45215
  exports.WIDGET_PROP_DENYLIST = denylist;
44921
45216
  exports.WidgetJsonifyDeprecated = WidgetJsonifyDeprecated;
44922
- exports.WidgetType = WidgetType;
44923
45217
  exports.Widgets = widgets$1;
44924
45218
  exports.addWidget = addWidget;
44925
45219
  exports.apiVersion = apiVersion;
@@ -44927,19 +45221,26 @@ exports.buildEmptyItemTreeForShape = buildEmptyItemTreeForShape;
44927
45221
  exports.buildMapper = buildMapper;
44928
45222
  exports.components = components;
44929
45223
  exports.constants = constants;
45224
+ exports.contentHasWidgetType = contentHasWidgetType;
44930
45225
  exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
44931
45226
  exports.displaySigFigs = displaySigFigs;
45227
+ exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
45228
+ exports.generateTestExpressionWidget = generateTestExpressionWidget;
45229
+ exports.generateTestInteractiveGraphWidget = generateTestInteractiveGraphWidget;
45230
+ exports.generateTestNumericInputWidget = generateTestNumericInputWidget;
44932
45231
  exports.generateTestPerseusItem = generateTestPerseusItem;
45232
+ exports.generateTestRadioWidget = generateTestRadioWidget;
44933
45233
  exports.genericPerseusItemData = genericPerseusItemData;
44934
- exports.getAllWidgetIds = getAllWidgetIds;
44935
- exports.getAllWidgetTypes = getAllWidgetTypes;
44936
45234
  exports.getAnswerFromUserInput = getAnswerFromUserInput;
44937
45235
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
44938
- exports.getCorrectAnswerForWidgetKey = getCorrectAnswerForWidgetKey;
44939
- exports.getQuestionWidgetIds = getQuestionWidgetIds;
44940
- exports.getValidWidgetKeys = getValidWidgetKeys;
44941
- exports.getWidgetTypeFromWidgetId = getWidgetTypeFromWidgetId;
44942
- exports.getWidgetTypeFromWidgetKey = getWidgetTypeFromWidgetKey;
45236
+ exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
45237
+ exports.getValidWidgetIds = getValidWidgetIds;
45238
+ exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
45239
+ exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
45240
+ exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
45241
+ exports.getWidgetTypeByWidgetId = getWidgetTypeByWidgetId;
45242
+ exports.getWidgetsFromWidgetMap = getWidgetsFromWidgetMap;
45243
+ exports.getWidgetsMapFromItemData = getWidgetsMapFromItemData;
44943
45244
  exports.globalConstants = globalConstants;
44944
45245
  exports.globalStyles = globalStyles;
44945
45246
  exports.icons = iconPaths;
@@ -44947,7 +45248,7 @@ exports.init = init;
44947
45248
  exports.injectWidgets = injectWidgets;
44948
45249
  exports.interactiveSizes = interactiveSizes$1;
44949
45250
  exports.isItemRenderableByVersion = isItemRenderableByVersion;
44950
- exports.isWidgetKeyInContent = isWidgetKeyInContent;
45251
+ exports.isWidgetIdInContent = isWidgetIdInContent;
44951
45252
  exports.isWrongAnswerSupported = isWrongAnswerSupported;
44952
45253
  exports.itemDataVersion = itemDataVersion;
44953
45254
  exports.itemToTree = itemToTree;