@khanacademy/perseus 43.0.1 → 44.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.
Files changed (78) hide show
  1. package/dist/all-widgets.d.ts +2 -23
  2. package/dist/basic-widgets.d.ts +2 -23
  3. package/dist/components/graph.d.ts +1 -0
  4. package/dist/components/icon.d.ts +0 -1
  5. package/dist/components/multi-button-group.d.ts +1 -0
  6. package/dist/components/number-input.d.ts +5 -3
  7. package/dist/components/text-input.d.ts +4 -2
  8. package/dist/components/tooltip.d.ts +1 -0
  9. package/dist/es/index.js +476 -261
  10. package/dist/es/index.js.map +1 -1
  11. package/dist/es/strings.js +4 -2
  12. package/dist/es/strings.js.map +1 -1
  13. package/dist/extra-widgets.d.ts +2 -23
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +465 -250
  16. package/dist/index.js.map +1 -1
  17. package/dist/interactive2/interactive-util.d.ts +1 -1
  18. package/dist/interactive2/movable-point-options.d.ts +2 -2
  19. package/dist/interactive2/movable-point.d.ts +7 -7
  20. package/dist/interactive2/objective_.d.ts +1 -1
  21. package/dist/jipt-paragraphs.d.ts +2 -2
  22. package/dist/perseus-api.d.ts +2 -77
  23. package/dist/perseus-types.d.ts +17 -17
  24. package/dist/server-item-renderer.d.ts +26 -49
  25. package/dist/strings.d.ts +1 -0
  26. package/dist/strings.js +4 -2
  27. package/dist/strings.js.map +1 -1
  28. package/dist/styles/global-styles.d.ts +0 -1
  29. package/dist/styles/shared.d.ts +0 -1
  30. package/dist/traversal.d.ts +1 -1
  31. package/dist/util/a11y.d.ts +0 -1
  32. package/dist/util/debounce.d.ts +1 -1
  33. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +1 -1
  34. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +2 -0
  35. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.d.ts +7 -0
  36. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.d.ts +1 -0
  37. package/dist/util/parse-perseus-json/general-purpose-parsers/unknown.d.ts +2 -0
  38. package/dist/util/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +2 -2
  39. package/dist/util/parse-perseus-json/perseus-parsers/images-map.d.ts +5 -0
  40. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +4 -0
  41. package/dist/validation.types.d.ts +14 -6
  42. package/dist/widgets/categorizer/categorizer.d.ts +2 -2
  43. package/dist/widgets/categorizer/score-categorizer.d.ts +2 -2
  44. package/dist/widgets/categorizer/validate-categorizer.d.ts +15 -0
  45. package/dist/widgets/definition/definition.d.ts +1 -1
  46. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
  47. package/dist/widgets/dropdown/dropdown.d.ts +9 -1
  48. package/dist/widgets/dropdown/validate-dropdown.d.ts +10 -0
  49. package/dist/widgets/explanation/explanation.d.ts +1 -1
  50. package/dist/widgets/expression/expression.d.ts +92 -217
  51. package/dist/widgets/image/image.d.ts +1 -1
  52. package/dist/widgets/interaction/interaction.d.ts +1 -1
  53. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +11 -7
  54. package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +7 -3
  55. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +1 -1
  56. package/dist/widgets/interactive-graphs/types.d.ts +1 -0
  57. package/dist/widgets/label-image/answer-pill.d.ts +0 -1
  58. package/dist/widgets/label-image/label-image.d.ts +5 -65
  59. package/dist/widgets/label-image/marker.d.ts +0 -1
  60. package/dist/widgets/matrix/matrix.d.ts +12 -5
  61. package/dist/widgets/measurer/measurer.d.ts +2 -2
  62. package/dist/widgets/molecule/molecule.d.ts +1 -1
  63. package/dist/widgets/orderer/validate-orderer.d.ts +12 -0
  64. package/dist/widgets/passage/passage-markdown.d.ts +2 -2
  65. package/dist/widgets/passage/passage.d.ts +1 -1
  66. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  67. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +1 -1
  68. package/dist/widgets/plotter/plotter.d.ts +2 -2
  69. package/dist/widgets/plotter/score-plotter.d.ts +2 -2
  70. package/dist/widgets/plotter/validate-plotter.d.ts +12 -0
  71. package/dist/widgets/radio/choice-icon/shared-styles.d.ts +0 -1
  72. package/dist/widgets/radio/choice-none-above.d.ts +1 -1
  73. package/dist/widgets/radio/radio.d.ts +3 -3
  74. package/dist/widgets/radio/validate-radio.d.ts +14 -0
  75. package/dist/widgets/sorter/validate-sorter.d.ts +13 -0
  76. package/dist/widgets/video/video.d.ts +1 -1
  77. package/package.json +5 -5
  78. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +0 -2
package/dist/index.js CHANGED
@@ -384,7 +384,8 @@ const mockStrings = {
384
384
  } = _ref28;
385
385
  return `Interactive elements: ${elements}`;
386
386
  },
387
- srNoInteractiveElements: "No interactive elements"
387
+ srNoInteractiveElements: "No interactive elements",
388
+ selectAnAnswer: "Select an answer"
388
389
  };
389
390
 
390
391
  /**
@@ -2882,8 +2883,6 @@ async function getImageSizeModern(url) {
2882
2883
  });
2883
2884
  }
2884
2885
 
2885
- /* eslint-disable @babel/no-invalid-this, getter-return, one-var */
2886
-
2887
2886
  // TODO: dedupe this with Coord in interactive2/types.js
2888
2887
 
2889
2888
  let supportsPassive = false;
@@ -2960,8 +2959,8 @@ function shuffle$3(array, randomSeed) {
2960
2959
  do {
2961
2960
  // Fischer-Yates shuffle
2962
2961
  for (let top = shuffled.length; top > 0; top--) {
2963
- const newEnd = Math.floor(random() * top),
2964
- temp = shuffled[newEnd];
2962
+ const newEnd = Math.floor(random() * top);
2963
+ const temp = shuffled[newEnd];
2965
2964
 
2966
2965
  // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
2967
2966
  shuffled[newEnd] = shuffled[top - 1];
@@ -3293,13 +3292,13 @@ function eq$4(x, y) {
3293
3292
  * Deep approximate equality on primitives, numbers, arrays, and objects.
3294
3293
  * Recursive.
3295
3294
  */
3296
- function deepEq$6(x, y) {
3295
+ function deepEq$7(x, y) {
3297
3296
  if (Array.isArray(x) && Array.isArray(y)) {
3298
3297
  if (x.length !== y.length) {
3299
3298
  return false;
3300
3299
  }
3301
3300
  for (let i = 0; i < x.length; i++) {
3302
- if (!deepEq$6(x[i], y[i])) {
3301
+ if (!deepEq$7(x[i], y[i])) {
3303
3302
  return false;
3304
3303
  }
3305
3304
  }
@@ -3317,10 +3316,10 @@ function deepEq$6(x, y) {
3317
3316
  if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
3318
3317
  return x === y || ___default["default"].all(x, function (v, k) {
3319
3318
  // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
3320
- return deepEq$6(y[k], v);
3319
+ return deepEq$7(y[k], v);
3321
3320
  }) && ___default["default"].all(y, function (v, k) {
3322
3321
  // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
3323
- return deepEq$6(x[k], v);
3322
+ return deepEq$7(x[k], v);
3324
3323
  });
3325
3324
  }
3326
3325
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
@@ -3599,7 +3598,7 @@ const Util = {
3599
3598
  tickStepFromNumTicks,
3600
3599
  constrainedTickStepsFromTickSteps,
3601
3600
  eq: eq$4,
3602
- deepEq: deepEq$6,
3601
+ deepEq: deepEq$7,
3603
3602
  parseQueryString,
3604
3603
  updateQueryString: updateQueryString$2,
3605
3604
  strongEncodeURIComponent,
@@ -9002,7 +9001,6 @@ var Tex$1 = {
9002
9001
  }
9003
9002
  };
9004
9003
 
9005
- /* eslint-disable @babel/no-invalid-this */
9006
9004
  const {
9007
9005
  processMath: processMath$1
9008
9006
  } = Tex$1;
@@ -10632,7 +10630,9 @@ const getClipPoint = function (graph, coord, angle) {
10632
10630
  return clipPoint;
10633
10631
  };
10634
10632
 
10635
- /* eslint-disable @babel/no-invalid-this */
10633
+ /**
10634
+ * Default methods for a wrapped movable.
10635
+ */
10636
10636
 
10637
10637
  /*
10638
10638
  * attr() and animate() simply pass the
@@ -10890,7 +10890,7 @@ class WrappedLine extends WrappedDrawing {
10890
10890
  */
10891
10891
  const {
10892
10892
  eq: eq$3,
10893
- deepEq: deepEq$5
10893
+ deepEq: deepEq$6
10894
10894
  } = Util;
10895
10895
 
10896
10896
  // This should really be a readonly tuple of [number, number]
@@ -11050,7 +11050,7 @@ function similar(coords1, coords2, tolerance) {
11050
11050
  angles = rotate(angles, i);
11051
11051
  // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
11052
11052
  sides = rotate(sides, i);
11053
- if (deepEq$5(angles1, angles)) {
11053
+ if (deepEq$6(angles1, angles)) {
11054
11054
  const sidePairs = ___default["default"].zip(sides1, sides);
11055
11055
  const factors = ___default["default"].map(sidePairs, function (pair) {
11056
11056
  return pair[0] / pair[1];
@@ -13989,7 +13989,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13989
13989
 
13990
13990
  /* eslint-disable @babel/no-invalid-this */
13991
13991
  const nestedMap$1 = Util.nestedMap;
13992
- const deepEq$4 = Util.deepEq;
13992
+ const deepEq$5 = Util.deepEq;
13993
13993
 
13994
13994
  /**
13995
13995
  * A base class for all Graphie Movables
@@ -14086,7 +14086,7 @@ const createSimpleClass = function (addFunction) {
14086
14086
  this._prevProps = this.props;
14087
14087
  },
14088
14088
  modify: function (graphie) {
14089
- if (!deepEq$4(this.props, this._prevProps)) {
14089
+ if (!deepEq$5(this.props, this._prevProps)) {
14090
14090
  this.remove();
14091
14091
  this.add(graphie);
14092
14092
  this._prevProps = this.props;
@@ -16744,7 +16744,7 @@ var Movables = {
16744
16744
  const GraphieMovable = GraphieClasses.GraphieMovable;
16745
16745
  const createGraphie = GraphUtils.createGraphie;
16746
16746
  const {
16747
- deepEq: deepEq$3,
16747
+ deepEq: deepEq$4,
16748
16748
  nestedMap
16749
16749
  } = Util;
16750
16750
  const {
@@ -16782,7 +16782,7 @@ class Graphie extends React__namespace.Component {
16782
16782
  if (this.props.setup !== prevProps.setup) {
16783
16783
  Log.error("<Graphie> was given a new setup function. " + "This is a bad idea; please refactor your code to give " + "the same setup function reference to <Graphie> on " + "every render.", perseusCore.Errors.Internal);
16784
16784
  }
16785
- if (!deepEq$3(this.props.options, prevProps.options) || !deepEq$3(this.props.box, prevProps.box) || !deepEq$3(this.props.range, prevProps.range)) {
16785
+ if (!deepEq$4(this.props.options, prevProps.options) || !deepEq$4(this.props.box, prevProps.box) || !deepEq$4(this.props.range, prevProps.range)) {
16786
16786
  this._setupGraphie();
16787
16787
  }
16788
16788
  this._updateMovables();
@@ -16830,7 +16830,7 @@ class Graphie extends React__namespace.Component {
16830
16830
  };
16831
16831
  _setupGraphie = () => {
16832
16832
  this._removeMovables();
16833
- const graphieDiv = ReactDOM__default["default"].findDOMNode(this.graphieDivRef.current);
16833
+ const graphieDiv = this.graphieDivRef.current;
16834
16834
  if (graphieDiv == null || graphieDiv instanceof Text) {
16835
16835
  throw new Error("No graphie container div found");
16836
16836
  }
@@ -18557,7 +18557,6 @@ const styles$C = aphrodite.StyleSheet.create({
18557
18557
  }
18558
18558
  });
18559
18559
 
18560
- /* eslint-disable no-useless-escape, no-prototype-builtins */
18561
18560
  const rules$1 = {
18562
18561
  ...pureMarkdown.pureMarkdownRules,
18563
18562
  // NOTE: basically ignored by JIPT. wraps everything at the outer layer
@@ -18734,7 +18733,7 @@ const rules$1 = {
18734
18733
  // The href prop should always be here, but just in case somehow the article's
18735
18734
  // link was not put together properly, let's make sure it's there so we
18736
18735
  // don't break the entire page.
18737
- const isKAUrl = href ? href.match(/https?:\/\/[^\/]*khanacademy.org|^\//) : false;
18736
+ const isKAUrl = href ? href.match(/https?:\/\/[^/]*khanacademy.org|^\//) : false;
18738
18737
  if (!isKAUrl) {
18739
18738
  // Prevents "reverse tabnabbing" phishing attacks
18740
18739
  // @ts-expect-error - TS2322 - Type '"noopener noreferrer"' is not assignable to type 'null'.
@@ -21144,6 +21143,7 @@ function uniqueIdForInput() {
21144
21143
  return `${prefix}${lastId}`;
21145
21144
  }
21146
21145
  class TextInput extends React__namespace.Component {
21146
+ inputRef = /*#__PURE__*/React__namespace.createRef();
21147
21147
  static defaultProps = {
21148
21148
  value: "",
21149
21149
  disabled: false
@@ -21157,33 +21157,32 @@ class TextInput extends React__namespace.Component {
21157
21157
  this.id = uniqueIdForInput();
21158
21158
  }
21159
21159
  }
21160
+ _getInput = () => {
21161
+ if (!this.inputRef.current) {
21162
+ throw new perseusCore.PerseusError("Input ref accessed before set", perseusCore.Errors.Internal);
21163
+ }
21164
+ return this.inputRef.current;
21165
+ };
21160
21166
  focus = () => {
21161
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'.
21162
- ReactDOM__default["default"].findDOMNode(this).focus();
21167
+ this._getInput().focus();
21163
21168
  };
21164
21169
  blur = () => {
21165
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'.
21166
- ReactDOM__default["default"].findDOMNode(this).blur();
21170
+ this._getInput().blur();
21167
21171
  };
21168
21172
  getValue = () => {
21169
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
21170
- return ReactDOM__default["default"].findDOMNode(this)?.value;
21173
+ return this.inputRef.current?.value;
21171
21174
  };
21172
21175
  getStringValue = () => {
21173
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
21174
- return ReactDOM__default["default"].findDOMNode(this)?.value.toString();
21176
+ return this.inputRef.current?.value.toString();
21175
21177
  };
21176
21178
  setSelectionRange = (selectionStart, selectionEnd) => {
21177
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'.
21178
- ReactDOM__default["default"].findDOMNode(this).setSelectionRange(selectionStart, selectionEnd);
21179
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
21179
21180
  };
21180
21181
  getSelectionStart = () => {
21181
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
21182
- return ReactDOM__default["default"].findDOMNode(this).selectionStart;
21182
+ return this._getInput().selectionStart;
21183
21183
  };
21184
21184
  getSelectionEnd = () => {
21185
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'.
21186
- return ReactDOM__default["default"].findDOMNode(this).selectionEnd;
21185
+ return this._getInput().selectionEnd;
21187
21186
  };
21188
21187
  render() {
21189
21188
  const {
@@ -21198,6 +21197,7 @@ class TextInput extends React__namespace.Component {
21198
21197
  } = this.props;
21199
21198
  const formattedValue = value === null ? "" : value.toString();
21200
21199
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksForm.TextField, {
21200
+ ref: this.inputRef,
21201
21201
  style: style,
21202
21202
  disabled: disabled,
21203
21203
  id: this.id,
@@ -21377,6 +21377,7 @@ const HORIZONTAL_ALIGNMNENTS = {
21377
21377
  * ```
21378
21378
  */
21379
21379
  class Tooltip extends React__namespace.Component {
21380
+ tooltipContainerRef = /*#__PURE__*/React__namespace.createRef();
21380
21381
  static defaultProps = {
21381
21382
  className: "",
21382
21383
  arrowSize: 10,
@@ -21459,8 +21460,7 @@ class Tooltip extends React__namespace.Component {
21459
21460
  display: this.props.show ? "block" : "none"
21460
21461
  }
21461
21462
  }, /*#__PURE__*/React__namespace.createElement("div", {
21462
- // eslint-disable-next-line react/no-string-refs
21463
- ref: "tooltipContainer",
21463
+ ref: this.tooltipContainerRef,
21464
21464
  className: "tooltipContainer",
21465
21465
  style: {
21466
21466
  position: "absolute",
@@ -21487,10 +21487,8 @@ class Tooltip extends React__namespace.Component {
21487
21487
  }, this.props.children.slice(1)), arrowBelow));
21488
21488
  }
21489
21489
  _updateHeight() {
21490
- const tooltipContainer = ReactDOM__default["default"].findDOMNode(
21491
- // eslint-disable-next-line react/no-string-refs
21492
- this.refs.tooltipContainer);
21493
- const height = tooltipContainer.offsetHeight;
21490
+ const tooltipContainer = this.tooltipContainerRef.current;
21491
+ const height = tooltipContainer?.offsetHeight || 0;
21494
21492
  if (height !== this.state.height) {
21495
21493
  this.setState({
21496
21494
  height
@@ -22219,7 +22217,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
22219
22217
  return value / 100;
22220
22218
  }
22221
22219
 
22222
- // Otherwise, we return input valuåe (number) stripped of the "%".
22220
+ // Otherwise, we return input value (number) stripped of the "%".
22223
22221
  return value;
22224
22222
  }
22225
22223
  function scoreNumericInput(userInput, rubric, strings) {
@@ -26277,7 +26275,15 @@ const styles$p = aphrodite.StyleSheet.create({
26277
26275
  }
26278
26276
  });
26279
26277
 
26280
- function scoreRadio(userInput, rubric, strings) {
26278
+ /**
26279
+ * Checks if the user has selected at least one option. Additional validation
26280
+ * is done in scoreRadio to check if the number of selected options is correct
26281
+ * and if the user has selected both a correct option and the "none of the above"
26282
+ * option.
26283
+ * @param userInput
26284
+ * @see `scoreRadio` for the additional validation logic and the scoring logic.
26285
+ */
26286
+ function validateRadio(userInput) {
26281
26287
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26282
26288
  return sum + (selected ? 1 : 0);
26283
26289
  }, 0);
@@ -26287,6 +26293,17 @@ function scoreRadio(userInput, rubric, strings) {
26287
26293
  message: null
26288
26294
  };
26289
26295
  }
26296
+ return null;
26297
+ }
26298
+
26299
+ function scoreRadio(userInput, rubric, strings) {
26300
+ const validationError = validateRadio(userInput);
26301
+ if (validationError) {
26302
+ return validationError;
26303
+ }
26304
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26305
+ return sum + (selected ? 1 : 0);
26306
+ }, 0);
26290
26307
  const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
26291
26308
  return currentChoice.correct ? sum + 1 : sum;
26292
26309
  }, 0);
@@ -26766,18 +26783,17 @@ const transform = (widgetOptions, strings, problemNum) => {
26766
26783
  };
26767
26784
  const propUpgrades$1 = {
26768
26785
  "1": v0props => {
26769
- let choices;
26770
- let hasNoneOfTheAbove;
26771
- if (!v0props.noneOfTheAbove) {
26772
- choices = v0props.choices;
26773
- hasNoneOfTheAbove = false;
26774
- } else {
26786
+ const {
26787
+ noneOfTheAbove,
26788
+ ...rest
26789
+ } = v0props;
26790
+ if (noneOfTheAbove) {
26775
26791
  throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
26776
26792
  }
26777
- return ___default["default"].extend(___default["default"].omit(v0props, "noneOfTheAbove"), {
26778
- choices: choices,
26779
- hasNoneOfTheAbove: hasNoneOfTheAbove
26780
- });
26793
+ return {
26794
+ ...rest,
26795
+ hasNoneOfTheAbove: false
26796
+ };
26781
26797
  }
26782
26798
  };
26783
26799
  var Radio = {
@@ -26814,23 +26830,36 @@ const getPromptJSON$n = (renderProps, userInput) => {
26814
26830
  };
26815
26831
  };
26816
26832
 
26817
- function scoreCategorizer(userInput, rubric, strings) {
26818
- let completed = true;
26819
- let allCorrect = true;
26820
- rubric.values.forEach((value, i) => {
26821
- if (userInput.values[i] == null) {
26822
- completed = false;
26823
- }
26824
- if (userInput.values[i] !== value) {
26825
- allCorrect = false;
26826
- }
26827
- });
26828
- if (!completed) {
26833
+ /**
26834
+ * Checks userInput from the categorizer widget to see if the user has selected
26835
+ * a category for each item.
26836
+ * @param userInput - The user's input corresponding to an array of indices that
26837
+ * represent the selected category for each row/item.
26838
+ * @param validationData - An array of strings corresponding to each row/item
26839
+ * @param strings - Used to provide a validation message
26840
+ */
26841
+ function validateCategorizer(userInput, validationData, strings) {
26842
+ const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
26843
+ if (incomplete) {
26829
26844
  return {
26830
26845
  type: "invalid",
26831
26846
  message: strings.invalidSelection
26832
26847
  };
26833
26848
  }
26849
+ return null;
26850
+ }
26851
+
26852
+ function scoreCategorizer(userInput, scoringData, strings) {
26853
+ const validationError = validateCategorizer(userInput, scoringData, strings);
26854
+ if (validationError) {
26855
+ return validationError;
26856
+ }
26857
+ let allCorrect = true;
26858
+ scoringData.values.forEach((value, i) => {
26859
+ if (userInput.values[i] !== value) {
26860
+ allCorrect = false;
26861
+ }
26862
+ });
26834
26863
  return {
26835
26864
  type: "points",
26836
26865
  earned: allCorrect ? 1 : 0,
@@ -27384,15 +27413,26 @@ const getPromptJSON$k = (renderProps, userInput) => {
27384
27413
  };
27385
27414
  };
27386
27415
 
27387
- function scoreDropdown(userInput, rubric) {
27388
- const selected = userInput.value;
27389
- if (selected === 0) {
27416
+ /**
27417
+ * Checks if the user has selected an item from the dropdown before scoring.
27418
+ * This is shown with a userInput value / index other than 0.
27419
+ */
27420
+ function validateDropdown(userInput) {
27421
+ if (userInput.value === 0) {
27390
27422
  return {
27391
27423
  type: "invalid",
27392
27424
  message: null
27393
27425
  };
27394
27426
  }
27395
- const correct = rubric.choices[selected - 1].correct;
27427
+ return null;
27428
+ }
27429
+
27430
+ function scoreDropdown(userInput, rubric) {
27431
+ const validationError = validateDropdown(userInput);
27432
+ if (validationError) {
27433
+ return validationError;
27434
+ }
27435
+ const correct = rubric.choices[userInput.value - 1].correct;
27396
27436
  return {
27397
27437
  type: "points",
27398
27438
  earned: correct ? 1 : 0,
@@ -27402,6 +27442,7 @@ function scoreDropdown(userInput, rubric) {
27402
27442
  }
27403
27443
 
27404
27444
  class Dropdown extends React__namespace.Component {
27445
+ static contextType = PerseusI18nContext;
27405
27446
  static defaultProps = {
27406
27447
  choices: [],
27407
27448
  selected: 0,
@@ -27444,26 +27485,44 @@ class Dropdown extends React__namespace.Component {
27444
27485
  value: String(i + 1),
27445
27486
  label: choice
27446
27487
  }))];
27447
- return /*#__PURE__*/React__namespace.createElement("div", {
27448
- // NOTE(jared): These are required to prevent weird behavior
27449
- // When there's a dropdown in a zoomable table.
27488
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
27489
+ scope: "dropdown-widget",
27490
+ mockOnFirstRender: true
27491
+ }, ids => /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
27492
+ // NOTE(jared): These are required to prevent weird behavior
27493
+ // When there's a dropdown in a zoomable table.
27494
+ , {
27450
27495
  onClick: e => {
27451
27496
  e.stopPropagation();
27452
27497
  },
27453
27498
  onTouchStart: e => {
27454
27499
  e.stopPropagation();
27455
27500
  }
27456
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27501
+ }, this.props.visibleLabel && /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelLarge, {
27502
+ tag: "label",
27503
+ id: ids.get("dropdown-label")
27504
+ }, this.props.visibleLabel), /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27505
+ id: ids.get("dropdown"),
27457
27506
  placeholder: "",
27458
27507
  onChange: value => this._handleChange(parseInt(value)),
27459
27508
  selectedValue: String(this.props.selected),
27460
- disabled: this.props.apiOptions.readOnly
27461
- }, children));
27509
+ disabled: this.props.apiOptions.readOnly,
27510
+ "aria-label": this.props.ariaLabel || this.context.strings.selectAnAnswer,
27511
+ "aria-labelledby": ids.get("dropdown-label")
27512
+ // This is currently necessary for SRs to read the labels properly.
27513
+ // However, WB is working on a change to add the "combobox" role to
27514
+ // all dropdowns.
27515
+ // See https://khanacademy.atlassian.net/browse/WB-1671
27516
+ ,
27517
+ role: "combobox"
27518
+ }, children)));
27462
27519
  }
27463
27520
  }
27464
27521
  const optionsTransform = widgetOptions => {
27465
27522
  return {
27466
27523
  placeholder: widgetOptions.placeholder,
27524
+ visibleLabel: widgetOptions.visibleLabel,
27525
+ ariaLabel: widgetOptions.ariaLabel,
27467
27526
  choices: widgetOptions.choices.map(choice => choice.content)
27468
27527
  };
27469
27528
  };
@@ -27782,8 +27841,6 @@ const getNextState = (currentState, answerable) => {
27782
27841
  }
27783
27842
  };
27784
27843
 
27785
- // exports has no 'transform'
27786
-
27787
27844
  // A Graded Group is more or less a Group widget that displays a check
27788
27845
  // answer button below the rendered content. When clicked, the widget grades
27789
27846
  // the stuff inside and displays feedback about whether the inputted answer was
@@ -28453,7 +28510,6 @@ const getPromptJSON$g = (renderProps, userInput) => {
28453
28510
  };
28454
28511
  };
28455
28512
 
28456
- /* eslint-disable one-var */
28457
28513
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
28458
28514
  const Plot$1 = Graphie.Plot;
28459
28515
  const DEFAULT_BACKGROUND_IMAGE = {
@@ -28554,14 +28610,14 @@ const Linear = ___default["default"].extend({}, PlotDefaults, {
28554
28610
  return [m, b];
28555
28611
  },
28556
28612
  getFunctionForCoeffs: function (coeffs, x) {
28557
- const m = coeffs[0],
28558
- b = coeffs[1];
28613
+ const m = coeffs[0];
28614
+ const b = coeffs[1];
28559
28615
  return m * x + b;
28560
28616
  },
28561
28617
  getEquationString: function (coords) {
28562
28618
  const coeffs = this.getCoefficients(coords);
28563
- const m = coeffs[0],
28564
- b = coeffs[1];
28619
+ const m = coeffs[0];
28620
+ const b = coeffs[1];
28565
28621
  return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
28566
28622
  }
28567
28623
  });
@@ -28585,9 +28641,9 @@ const Quadratic = ___default["default"].extend({}, PlotDefaults, {
28585
28641
  return [a, b, c];
28586
28642
  },
28587
28643
  getFunctionForCoeffs: function (coeffs, x) {
28588
- const a = coeffs[0],
28589
- b = coeffs[1],
28590
- c = coeffs[2];
28644
+ const a = coeffs[0];
28645
+ const b = coeffs[1];
28646
+ const c = coeffs[2];
28591
28647
  return (a * x + b) * x + c;
28592
28648
  },
28593
28649
  getPropsForCoeffs: function (coeffs) {
@@ -28599,9 +28655,9 @@ const Quadratic = ___default["default"].extend({}, PlotDefaults, {
28599
28655
  },
28600
28656
  getEquationString: function (coords) {
28601
28657
  const coeffs = this.getCoefficients(coords);
28602
- const a = coeffs[0],
28603
- b = coeffs[1],
28604
- c = coeffs[2];
28658
+ const a = coeffs[0];
28659
+ const b = coeffs[1];
28660
+ const c = coeffs[2];
28605
28661
  return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
28606
28662
  }
28607
28663
  });
@@ -28620,10 +28676,10 @@ const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
28620
28676
  return [a, b, c, d];
28621
28677
  },
28622
28678
  getFunctionForCoeffs: function (coeffs, x) {
28623
- const a = coeffs[0],
28624
- b = coeffs[1],
28625
- c = coeffs[2],
28626
- d = coeffs[3];
28679
+ const a = coeffs[0];
28680
+ const b = coeffs[1];
28681
+ const c = coeffs[2];
28682
+ const d = coeffs[3];
28627
28683
  return a * Math.sin(b * x - c) + d;
28628
28684
  },
28629
28685
  getPropsForCoeffs: function (coeffs) {
@@ -28636,10 +28692,10 @@ const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
28636
28692
  },
28637
28693
  getEquationString: function (coords) {
28638
28694
  const coeffs = this.getCoefficients(coords);
28639
- const a = coeffs[0],
28640
- b = coeffs[1],
28641
- c = coeffs[2],
28642
- d = coeffs[3];
28695
+ const a = coeffs[0];
28696
+ const b = coeffs[1];
28697
+ const c = coeffs[2];
28698
+ const d = coeffs[3];
28643
28699
  return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28644
28700
  },
28645
28701
  areEqual: function (coeffs1, coeffs2) {
@@ -28659,18 +28715,18 @@ const Tangent = ___default["default"].extend({}, PlotDefaults, {
28659
28715
  return [a, b, c, d];
28660
28716
  },
28661
28717
  getFunctionForCoeffs: function (coeffs, x) {
28662
- const a = coeffs[0],
28663
- b = coeffs[1],
28664
- c = coeffs[2],
28665
- d = coeffs[3];
28718
+ const a = coeffs[0];
28719
+ const b = coeffs[1];
28720
+ const c = coeffs[2];
28721
+ const d = coeffs[3];
28666
28722
  return a * Math.tan(b * x - c) + d;
28667
28723
  },
28668
28724
  getEquationString: function (coords) {
28669
28725
  const coeffs = this.getCoefficients(coords);
28670
- const a = coeffs[0],
28671
- b = coeffs[1],
28672
- c = coeffs[2],
28673
- d = coeffs[3];
28726
+ const a = coeffs[0];
28727
+ const b = coeffs[1];
28728
+ const c = coeffs[2];
28729
+ const d = coeffs[3];
28674
28730
  return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28675
28731
  },
28676
28732
  areEqual: function (coeffs1, coeffs2) {
@@ -28729,9 +28785,9 @@ const Exponential = ___default["default"].extend({}, PlotDefaults, {
28729
28785
  return [a, b, c];
28730
28786
  },
28731
28787
  getFunctionForCoeffs: function (coeffs, x) {
28732
- const a = coeffs[0],
28733
- b = coeffs[1],
28734
- c = coeffs[2];
28788
+ const a = coeffs[0];
28789
+ const b = coeffs[1];
28790
+ const c = coeffs[2];
28735
28791
  return a * Math.exp(b * x) + c;
28736
28792
  },
28737
28793
  getEquationString: function (coords, asymptote) {
@@ -28739,9 +28795,9 @@ const Exponential = ___default["default"].extend({}, PlotDefaults, {
28739
28795
  return null;
28740
28796
  }
28741
28797
  const coeffs = this.getCoefficients(coords, asymptote);
28742
- const a = coeffs[0],
28743
- b = coeffs[1],
28744
- c = coeffs[2];
28798
+ const a = coeffs[0];
28799
+ const b = coeffs[1];
28800
+ const c = coeffs[2];
28745
28801
  return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
28746
28802
  }
28747
28803
  });
@@ -28786,9 +28842,9 @@ const Logarithm = ___default["default"].extend({}, PlotDefaults, {
28786
28842
  }
28787
28843
  },
28788
28844
  getFunctionForCoeffs: function (coeffs, x, asymptote) {
28789
- const a = coeffs[0],
28790
- b = coeffs[1],
28791
- c = coeffs[2];
28845
+ const a = coeffs[0];
28846
+ const b = coeffs[1];
28847
+ const c = coeffs[2];
28792
28848
  return a * Math.log(b * x + c);
28793
28849
  },
28794
28850
  getEquationString: function (coords, asymptote) {
@@ -28796,9 +28852,9 @@ const Logarithm = ___default["default"].extend({}, PlotDefaults, {
28796
28852
  return null;
28797
28853
  }
28798
28854
  const coeffs = this.getCoefficients(coords, asymptote);
28799
- const a = coeffs[0],
28800
- b = coeffs[1],
28801
- c = coeffs[2];
28855
+ const a = coeffs[0];
28856
+ const b = coeffs[1];
28857
+ const c = coeffs[2];
28802
28858
  return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
28803
28859
  }
28804
28860
  });
@@ -28822,16 +28878,16 @@ const AbsoluteValue = ___default["default"].extend({}, PlotDefaults, {
28822
28878
  return [m, horizontalOffset, verticalOffset];
28823
28879
  },
28824
28880
  getFunctionForCoeffs: function (coeffs, x) {
28825
- const m = coeffs[0],
28826
- horizontalOffset = coeffs[1],
28827
- verticalOffset = coeffs[2];
28881
+ const m = coeffs[0];
28882
+ const horizontalOffset = coeffs[1];
28883
+ const verticalOffset = coeffs[2];
28828
28884
  return m * Math.abs(x - horizontalOffset) + verticalOffset;
28829
28885
  },
28830
28886
  getEquationString: function (coords) {
28831
28887
  const coeffs = this.getCoefficients(coords);
28832
- const m = coeffs[0],
28833
- horizontalOffset = coeffs[1],
28834
- verticalOffset = coeffs[2];
28888
+ const m = coeffs[0];
28889
+ const horizontalOffset = coeffs[1];
28890
+ const verticalOffset = coeffs[2];
28835
28891
  return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
28836
28892
  }
28837
28893
  });
@@ -30592,6 +30648,7 @@ function numSteps$1(range, step) {
30592
30648
  return Math.floor((range[1] - range[0]) / step);
30593
30649
  }
30594
30650
  class Graph extends React__namespace.Component {
30651
+ graphieDivRef = /*#__PURE__*/React__namespace.createRef();
30595
30652
  protractor;
30596
30653
  ruler;
30597
30654
  _graphie;
@@ -30671,9 +30728,7 @@ class Graph extends React__namespace.Component {
30671
30728
  if (this._hasSetupGraphieThisUpdate) {
30672
30729
  return;
30673
30730
  }
30674
-
30675
- // eslint-disable-next-line react/no-string-refs
30676
- const graphieDiv = ReactDOM__default["default"].findDOMNode(this.refs.graphieDiv);
30731
+ const graphieDiv = this.graphieDivRef.current;
30677
30732
  // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'empty' does not exist on type 'JQueryStatic'.
30678
30733
  $__default["default"](graphieDiv).empty();
30679
30734
 
@@ -30862,7 +30917,7 @@ class Graph extends React__namespace.Component {
30862
30917
  onClick: this.onClick
30863
30918
  }, image, /*#__PURE__*/React__namespace.createElement("div", {
30864
30919
  className: "graphie",
30865
- ref: "graphieDiv"
30920
+ ref: this.graphieDivRef
30866
30921
  }));
30867
30922
  }
30868
30923
  }
@@ -31948,13 +32003,13 @@ class InfoTip extends React__namespace.Component {
31948
32003
  * this component allows multiple selection!
31949
32004
  */
31950
32005
  class MultiButtonGroup extends React__namespace.Component {
32006
+ buttonContainerRef = /*#__PURE__*/React__namespace.createRef();
31951
32007
  static defaultProps = {
31952
32008
  values: [],
31953
32009
  allowEmpty: true
31954
32010
  };
31955
32011
  focus() {
31956
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
31957
- ReactDOM__namespace.findDOMNode(this)?.focus();
32012
+ this.buttonContainerRef.current?.focus();
31958
32013
  return true;
31959
32014
  }
31960
32015
  toggleSelect = newValue => {
@@ -31989,7 +32044,8 @@ class MultiButtonGroup extends React__namespace.Component {
31989
32044
  display: "inline-block"
31990
32045
  };
31991
32046
  return /*#__PURE__*/React__namespace.createElement("div", {
31992
- style: outerStyle
32047
+ style: outerStyle,
32048
+ ref: this.buttonContainerRef
31993
32049
  }, buttons);
31994
32050
  }
31995
32051
  }
@@ -32054,6 +32110,7 @@ const getNumericFormat = KhanMath.getNumericFormat;
32054
32110
  */
32055
32111
  class NumberInput extends React__namespace.Component {
32056
32112
  static contextType = PerseusI18nContext;
32113
+ inputRef = /*#__PURE__*/React__namespace.createRef();
32057
32114
  static propTypes = {
32058
32115
  value: PropTypes__default["default"].number,
32059
32116
  format: PropTypes__default["default"].string,
@@ -32080,23 +32137,24 @@ class NumberInput extends React__namespace.Component {
32080
32137
  this._setValue(this.props.value, this.state.format);
32081
32138
  }
32082
32139
  }
32140
+ _getInput = () => {
32141
+ if (!this.inputRef.current) {
32142
+ throw new perseusCore.PerseusError("Input ref accessed before set", perseusCore.Errors.Internal);
32143
+ }
32144
+ return this.inputRef.current;
32145
+ };
32083
32146
 
32084
32147
  /* Return the current "value" of this input
32085
32148
  * If empty, it returns the placeholder (if it is a number) or null
32086
32149
  */
32087
32150
  getValue = () => {
32088
- return this.parseInputValue(
32089
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
32090
- ReactDOM__default["default"].findDOMNode(this.refs.input).value // eslint-disable-line react/no-string-refs
32091
- );
32151
+ return this.parseInputValue(this._getInput().value);
32092
32152
  };
32093
32153
 
32094
32154
  /* Return the current string value of this input */
32095
32155
  getStringValue = () => {
32096
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
32097
- return ReactDOM__default["default"].findDOMNode(this.refs.input).value.toString(); // eslint-disable-line react/no-string-refs
32156
+ return this._getInput().toString();
32098
32157
  };
32099
-
32100
32158
  parseInputValue = value => {
32101
32159
  if (value === "") {
32102
32160
  const placeholder = this.props.placeholder;
@@ -32108,26 +32166,21 @@ class NumberInput extends React__namespace.Component {
32108
32166
 
32109
32167
  /* Set text input focus to this input */
32110
32168
  focus = () => {
32111
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'.
32112
- ReactDOM__default["default"].findDOMNode(this.refs.input).focus(); // eslint-disable-line react/no-string-refs
32169
+ this._getInput().focus();
32113
32170
  this._handleFocus();
32114
32171
  };
32115
32172
  blur = () => {
32116
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'.
32117
- ReactDOM__default["default"].findDOMNode(this.refs.input).blur(); // eslint-disable-line react/no-string-refs
32173
+ this._getInput().blur();
32118
32174
  this._handleBlur();
32119
32175
  };
32120
32176
  setSelectionRange = (selectionStart, selectionEnd) => {
32121
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'.
32122
- ReactDOM__default["default"].findDOMNode(this).setSelectionRange(selectionStart, selectionEnd);
32177
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
32123
32178
  };
32124
32179
  getSelectionStart = () => {
32125
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
32126
- return ReactDOM__default["default"].findDOMNode(this).selectionStart;
32180
+ return this._getInput().selectionStart;
32127
32181
  };
32128
32182
  getSelectionEnd = () => {
32129
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'.
32130
- return ReactDOM__default["default"].findDOMNode(this).selectionEnd;
32183
+ return this._getInput().selectionEnd;
32131
32184
  };
32132
32185
  _checkValidity = value => {
32133
32186
  if (value == null) {
@@ -32187,9 +32240,7 @@ class NumberInput extends React__namespace.Component {
32187
32240
  }
32188
32241
  };
32189
32242
  _setValue = (val, format) => {
32190
- // eslint-disable-next-line react/no-string-refs
32191
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'val' does not exist on type 'JQueryStatic'.
32192
- $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.input)).val(toNumericString(val, format));
32243
+ this._getInput().value = toNumericString(val, format);
32193
32244
  };
32194
32245
  render() {
32195
32246
  let classes = classNames__default["default"]({
@@ -32213,10 +32264,8 @@ class NumberInput extends React__namespace.Component {
32213
32264
  } = this.props;
32214
32265
  return /*#__PURE__*/React__namespace.createElement("input", _extends({}, restProps, {
32215
32266
  className: classes,
32216
- type: "text"
32217
- // eslint-disable-next-line react/no-string-refs
32218
- ,
32219
- ref: "input",
32267
+ type: "text",
32268
+ ref: this.inputRef,
32220
32269
  onChange: this._handleChange,
32221
32270
  onFocus: this._handleFocus,
32222
32271
  onBlur: this._handleBlur,
@@ -33711,15 +33760,18 @@ function useControlPoint(params) {
33711
33760
  } = useGraphConfig();
33712
33761
  const {
33713
33762
  point,
33714
- sequenceNumber,
33763
+ ariaDescribedBy,
33764
+ ariaLabel,
33765
+ ariaLive = "polite",
33715
33766
  color,
33716
- cursor,
33717
33767
  constrain = p => snap(snapStep, p),
33768
+ cursor,
33769
+ forwardedRef = noop$1,
33770
+ sequenceNumber = 1,
33718
33771
  onMove = noop$1,
33719
33772
  onClick = noop$1,
33720
33773
  onFocus = noop$1,
33721
- onBlur = noop$1,
33722
- forwardedRef = noop$1
33774
+ onBlur = noop$1
33723
33775
  } = params;
33724
33776
  const {
33725
33777
  strings,
@@ -33742,6 +33794,13 @@ function useControlPoint(params) {
33742
33794
  onMove,
33743
33795
  constrainKeyboardMovement: constrain
33744
33796
  });
33797
+
33798
+ // if custom aria label is not provided, will use default of sequence number and point coordinates
33799
+ const pointAriaLabel = ariaLabel || strings.srPointAtCoordinates({
33800
+ num: sequenceNumber,
33801
+ x: srFormatNumber(point[X], locale),
33802
+ y: srFormatNumber(point[Y], locale)
33803
+ });
33745
33804
  React.useLayoutEffect(() => {
33746
33805
  setForwardedRef(forwardedRef, focusableHandleRef.current);
33747
33806
  }, [forwardedRef]);
@@ -33751,15 +33810,9 @@ function useControlPoint(params) {
33751
33810
  tabIndex: disableKeyboardInteraction ? -1 : 0,
33752
33811
  ref: focusableHandleRef,
33753
33812
  role: "button",
33754
- "aria-label": strings.srPointAtCoordinates({
33755
- num: sequenceNumber,
33756
- x: srFormatNumber(point[X], locale),
33757
- y: srFormatNumber(point[Y], locale)
33758
- })
33759
- // aria-live="assertive" causes the new location of the point to be
33760
- // announced immediately on move.
33761
- ,
33762
- "aria-live": "assertive",
33813
+ "aria-describedby": ariaDescribedBy,
33814
+ "aria-label": pointAriaLabel,
33815
+ "aria-live": ariaLive,
33763
33816
  onFocus: event => {
33764
33817
  onFocus(event);
33765
33818
  setFocused(true);
@@ -36835,7 +36888,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36835
36888
  });
36836
36889
 
36837
36890
  const eq$1 = Util.eq;
36838
- const deepEq$2 = Util.deepEq;
36891
+ const deepEq$3 = Util.deepEq;
36839
36892
  function scoreInteractiveGraph(userInput, rubric) {
36840
36893
  // None-type graphs are not graded
36841
36894
  if (userInput.type === "none" && rubric.correct.type === "none") {
@@ -36885,7 +36938,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36885
36938
  // If the parabola coefficients match, it's correct.
36886
36939
  const guessCoeffs = getQuadraticCoefficients(userInput.coords);
36887
36940
  const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
36888
- if (deepEq$2(guessCoeffs, correctCoeffs)) {
36941
+ if (deepEq$3(guessCoeffs, correctCoeffs)) {
36889
36942
  return {
36890
36943
  type: "points",
36891
36944
  earned: 1,
@@ -36899,7 +36952,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36899
36952
  const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
36900
36953
  const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
36901
36954
  // If the canonical coefficients match, it's correct.
36902
- if (deepEq$2(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36955
+ if (deepEq$3(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36903
36956
  return {
36904
36957
  type: "points",
36905
36958
  earned: 1,
@@ -36908,7 +36961,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36908
36961
  };
36909
36962
  }
36910
36963
  } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
36911
- if (deepEq$2(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36964
+ if (deepEq$3(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36912
36965
  return {
36913
36966
  type: "points",
36914
36967
  earned: 1,
@@ -36930,7 +36983,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36930
36983
  guess?.sort();
36931
36984
  // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
36932
36985
  correct.sort();
36933
- if (deepEq$2(guess, correct)) {
36986
+ if (deepEq$3(guess, correct)) {
36934
36987
  return {
36935
36988
  type: "points",
36936
36989
  earned: 1,
@@ -36952,7 +37005,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36952
37005
  /* exact */
36953
37006
  guess.sort();
36954
37007
  correct.sort();
36955
- match = deepEq$2(guess, correct);
37008
+ match = deepEq$3(guess, correct);
36956
37009
  }
36957
37010
  if (match) {
36958
37011
  return {
@@ -36967,7 +37020,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36967
37020
  let correct = Util.deepClone(rubric.correct.coords);
36968
37021
  guess = ___default["default"].invoke(guess, "sort").sort();
36969
37022
  correct = ___default["default"].invoke(correct, "sort").sort();
36970
- if (deepEq$2(guess, correct)) {
37023
+ if (deepEq$3(guess, correct)) {
36971
37024
  return {
36972
37025
  type: "points",
36973
37026
  earned: 1,
@@ -36978,7 +37031,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36978
37031
  } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
36979
37032
  const guess = userInput.coords;
36980
37033
  const correct = rubric.correct.coords;
36981
- if (deepEq$2(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
37034
+ if (deepEq$3(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
36982
37035
  return {
36983
37036
  type: "points",
36984
37037
  earned: 1,
@@ -37005,7 +37058,7 @@ function scoreInteractiveGraph(userInput, rubric) {
37005
37058
  /* exact */
37006
37059
  match =
37007
37060
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
37008
- deepEq$2(guess[1], correct[1]) &&
37061
+ deepEq$3(guess[1], correct[1]) &&
37009
37062
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
37010
37063
  collinear(correct[1], correct[0], guess[0]) &&
37011
37064
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
@@ -41267,14 +41320,20 @@ class Measurer extends React__namespace.Component {
41267
41320
  }
41268
41321
  const propUpgrades = {
41269
41322
  "1": v0props => {
41270
- const v1props = ___default["default"](v0props).chain().omit("imageUrl", "imageTop", "imageLeft").extend({
41323
+ const {
41324
+ imageUrl,
41325
+ imageTop,
41326
+ imageLeft,
41327
+ ...rest
41328
+ } = v0props;
41329
+ return {
41330
+ ...rest,
41271
41331
  image: {
41272
- url: v0props.imageUrl,
41273
- top: v0props.imageTop,
41274
- left: v0props.imageLeft
41332
+ url: imageUrl,
41333
+ top: imageTop,
41334
+ left: imageLeft
41275
41335
  }
41276
- }).value();
41277
- return v1props;
41336
+ };
41278
41337
  }
41279
41338
  };
41280
41339
  var Measurer$1 = {
@@ -42808,14 +42867,28 @@ const getPromptJSON$5 = (renderProps, userInput) => {
42808
42867
  };
42809
42868
  };
42810
42869
 
42811
- function scoreOrderer(userInput, rubric) {
42870
+ /**
42871
+ * Checks user input from the orderer widget to see if the user has started
42872
+ * ordering the options, making the widget scorable.
42873
+ * @param userInput
42874
+ * @see `scoreOrderer` for more details.
42875
+ */
42876
+ function validateOrderer(userInput) {
42812
42877
  if (userInput.current.length === 0) {
42813
42878
  return {
42814
42879
  type: "invalid",
42815
42880
  message: null
42816
42881
  };
42817
42882
  }
42818
- const correct = ___default["default"].isEqual(userInput.current, ___default["default"].pluck(rubric.correctOptions, "content"));
42883
+ return null;
42884
+ }
42885
+
42886
+ function scoreOrderer(userInput, rubric) {
42887
+ const validateError = validateOrderer(userInput);
42888
+ if (validateError) {
42889
+ return validateError;
42890
+ }
42891
+ const correct = ___default["default"].isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
42819
42892
  return {
42820
42893
  type: "points",
42821
42894
  earned: correct ? 1 : 0,
@@ -42991,15 +43064,13 @@ class Card extends React__namespace.Component {
42991
43064
  }))));
42992
43065
  }
42993
43066
  }
42994
- const NORMAL = "normal";
42995
- const HORIZONTAL = "horizontal";
42996
43067
  class Orderer extends React__namespace.Component {
42997
43068
  static defaultProps = {
42998
43069
  current: [],
42999
43070
  options: [],
43000
43071
  correctOptions: [],
43001
- height: NORMAL,
43002
- layout: HORIZONTAL,
43072
+ height: "normal",
43073
+ layout: "horizontal",
43003
43074
  linterContext: PerseusLinter.linterContextDefault
43004
43075
  };
43005
43076
  state = {
@@ -43152,7 +43223,7 @@ class Orderer extends React__namespace.Component {
43152
43223
  };
43153
43224
  findCorrectIndex = (draggable, list) => {
43154
43225
  // Find the correct index for a card given the current cards.
43155
- const isHorizontal = this.props.layout === HORIZONTAL;
43226
+ const isHorizontal = this.props.layout === "horizontal";
43156
43227
  // eslint-disable-next-line react/no-string-refs
43157
43228
  // @ts-expect-error - TS2769 - No overload matches this call.
43158
43229
  const $dragList = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.dragList));
@@ -43202,28 +43273,24 @@ class Orderer extends React__namespace.Component {
43202
43273
  if (draggable == null) {
43203
43274
  return false;
43204
43275
  }
43205
- const isHorizontal = this.props.layout === HORIZONTAL,
43206
- // @ts-expect-error - TS2769 - No overload matches this call.
43207
- $draggable = $__default["default"](ReactDOM__default["default"].findDOMNode(draggable)),
43208
- // eslint-disable-next-line react/no-string-refs
43209
- // @ts-expect-error - TS2769 - No overload matches this call.
43210
- $bank = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.bank)),
43211
- // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43212
- draggableOffset = $draggable.offset(),
43213
- // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43214
- bankOffset = $bank.offset(),
43215
- // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43216
- draggableHeight = $draggable.outerHeight(true),
43217
- // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43218
- bankHeight = $bank.outerHeight(true),
43219
- // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43220
- bankWidth = $bank.outerWidth(true),
43221
- // eslint-disable-next-line react/no-string-refs
43222
- dragList = ReactDOM__default["default"].findDOMNode(this.refs.dragList);
43223
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'width' does not exist on type 'JQueryStatic'.
43224
- $__default["default"](dragList).width();
43225
- const // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43226
- draggableWidth = $draggable.outerWidth(true);
43276
+ const isHorizontal = this.props.layout === "horizontal";
43277
+ // @ts-expect-error - TS2769 - No overload matches this call.
43278
+ const $draggable = $__default["default"](ReactDOM__default["default"].findDOMNode(draggable));
43279
+ // eslint-disable-next-line react/no-string-refs
43280
+ // @ts-expect-error - TS2769 - No overload matches this call.
43281
+ const $bank = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.bank));
43282
+ // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43283
+ const draggableOffset = $draggable.offset();
43284
+ // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43285
+ const bankOffset = $bank.offset();
43286
+ // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43287
+ const draggableHeight = $draggable.outerHeight(true);
43288
+ // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43289
+ const bankHeight = $bank.outerHeight(true);
43290
+ // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43291
+ const bankWidth = $bank.outerWidth(true);
43292
+ // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43293
+ const draggableWidth = $draggable.outerWidth(true);
43227
43294
  if (isHorizontal) {
43228
43295
  return draggableOffset.top + draggableHeight / 2 < bankOffset.top + bankHeight;
43229
43296
  }
@@ -43642,18 +43709,36 @@ const getPromptJSON$3 = () => {
43642
43709
  };
43643
43710
 
43644
43711
  const {
43645
- deepEq: deepEq$1
43712
+ deepEq: deepEq$2
43646
43713
  } = Util;
43647
- function scorePlotter(userInput, rubric) {
43648
- if (deepEq$1(userInput, rubric.starting)) {
43714
+
43715
+ /**
43716
+ * Checks user input to confirm it is not the same as the starting values for the graph.
43717
+ * This means the user has modified the graph, and the question can be scored.
43718
+ *
43719
+ * @see 'scorePlotter' for more details on scoring.
43720
+ */
43721
+ function validatePlotter(userInput, validationData) {
43722
+ if (deepEq$2(userInput, validationData.starting)) {
43649
43723
  return {
43650
43724
  type: "invalid",
43651
43725
  message: null
43652
43726
  };
43653
43727
  }
43728
+ return null;
43729
+ }
43730
+
43731
+ const {
43732
+ deepEq: deepEq$1
43733
+ } = Util;
43734
+ function scorePlotter(userInput, scoringData) {
43735
+ const validationError = validatePlotter(userInput, scoringData);
43736
+ if (validationError) {
43737
+ return validationError;
43738
+ }
43654
43739
  return {
43655
43740
  type: "points",
43656
- earned: deepEq$1(userInput, rubric.correct) ? 1 : 0,
43741
+ earned: deepEq$1(userInput, scoringData.correct) ? 1 : 0,
43657
43742
  total: 1,
43658
43743
  message: null
43659
43744
  };
@@ -44540,7 +44625,14 @@ const getPromptJSON$1 = userInput => {
44540
44625
  };
44541
44626
  };
44542
44627
 
44543
- function scoreSorter(userInput, rubric) {
44628
+ /**
44629
+ * Checks user input for the sorter widget to ensure that the user has made
44630
+ * changes before attempting to score the widget.
44631
+ * @param userInput
44632
+ * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
44633
+ * for more details on how the sorter widget is scored.
44634
+ */
44635
+ function validateSorter(userInput) {
44544
44636
  // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44545
44637
  // prevents the "Check" button from becoming active. We want the user
44546
44638
  // to make a change before trying to move forward. This makes an
@@ -44553,6 +44645,14 @@ function scoreSorter(userInput, rubric) {
44553
44645
  message: null
44554
44646
  };
44555
44647
  }
44648
+ return null;
44649
+ }
44650
+
44651
+ function scoreSorter(userInput, rubric) {
44652
+ const validationError = validateSorter(userInput);
44653
+ if (validationError) {
44654
+ return validationError;
44655
+ }
44556
44656
  const correct = Util.deepEq(userInput.options, rubric.correct);
44557
44657
  return {
44558
44658
  type: "points",
@@ -45127,7 +45227,7 @@ const init = function () {
45127
45227
 
45128
45228
  // This file is processed by a Rollup plugin (replace) to inject the production
45129
45229
  const libName = "@khanacademy/perseus";
45130
- const libVersion = "43.0.1";
45230
+ const libVersion = "44.0.0";
45131
45231
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45132
45232
 
45133
45233
  const apiVersion = {
@@ -45412,7 +45512,6 @@ const styles$3 = aphrodite.StyleSheet.create({
45412
45512
  }
45413
45513
  });
45414
45514
 
45415
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
45416
45515
  // Use of UNSAFE_componentWillReceiveProps:
45417
45516
  // eslint-disable-next-line react/no-unsafe
45418
45517
  class HintsRenderer extends React__namespace.Component {
@@ -48638,6 +48737,27 @@ function pair(parseA, parseB) {
48638
48737
  };
48639
48738
  }
48640
48739
 
48740
+ function pipeParsers(p) {
48741
+ return new ParserPipeline(p);
48742
+ }
48743
+ class ParserPipeline {
48744
+ constructor(parser) {
48745
+ this.parser = parser;
48746
+ }
48747
+ then(nextParser) {
48748
+ return new ParserPipeline(composeParsers(this.parser, nextParser));
48749
+ }
48750
+ }
48751
+ function composeParsers(parserA, parserB) {
48752
+ return (rawValue, ctx) => {
48753
+ const partialResult = parserA(rawValue, ctx);
48754
+ if (isFailure(partialResult)) {
48755
+ return partialResult;
48756
+ }
48757
+ return parserB(partialResult.value, ctx);
48758
+ };
48759
+ }
48760
+
48641
48761
  function record(parseKey, parseValue) {
48642
48762
  return (rawValue, ctx) => {
48643
48763
  if (!isObject(rawValue)) {
@@ -48718,6 +48838,20 @@ function either(parseA, parseB) {
48718
48838
  };
48719
48839
  }
48720
48840
 
48841
+ function defaulted(parser, fallback) {
48842
+ return (rawValue, ctx) => {
48843
+ if (rawValue == null) {
48844
+ return success(fallback(rawValue));
48845
+ }
48846
+ return parser(rawValue, ctx);
48847
+ };
48848
+ }
48849
+
48850
+ const parseImages = defaulted(record(string, object({
48851
+ width: number,
48852
+ height: number
48853
+ })), () => ({}));
48854
+
48721
48855
  function parseWidget(parseType, parseOptions) {
48722
48856
  return object({
48723
48857
  type: parseType,
@@ -48726,10 +48860,21 @@ function parseWidget(parseType, parseOptions) {
48726
48860
  alignment: optional(string),
48727
48861
  options: parseOptions,
48728
48862
  key: optional(number),
48729
- version: object({
48863
+ version: optional(object({
48730
48864
  major: number,
48731
48865
  minor: number
48732
- })
48866
+ }))
48867
+ });
48868
+ }
48869
+ function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
48870
+ return object({
48871
+ type: parseType,
48872
+ static: optional(boolean),
48873
+ graded: optional(boolean),
48874
+ alignment: optional(string),
48875
+ options: parseOptions,
48876
+ key: optional(number),
48877
+ version: parseVersion
48733
48878
  });
48734
48879
  }
48735
48880
 
@@ -48737,7 +48882,7 @@ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48737
48882
  items: array(string),
48738
48883
  categories: array(string),
48739
48884
  randomizeItems: boolean,
48740
- static: boolean,
48885
+ static: defaulted(boolean, () => false),
48741
48886
  values: array(number),
48742
48887
  highlightLint: optional(boolean),
48743
48888
  linterContext: optional(object({
@@ -48769,7 +48914,9 @@ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48769
48914
 
48770
48915
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48771
48916
  placeholder: string,
48772
- static: boolean,
48917
+ ariaLabel: optional(string),
48918
+ visibleLabel: optional(string),
48919
+ static: defaulted(boolean, () => false),
48773
48920
  choices: array(object({
48774
48921
  content: string,
48775
48922
  correct: boolean
@@ -48792,9 +48939,12 @@ const parseAnswerForm = object({
48792
48939
  form: boolean,
48793
48940
  simplify: boolean,
48794
48941
  considered: enumeration("correct", "wrong", "ungraded"),
48795
- key: optional(string)
48942
+ key: pipeParsers(optional(union(string).or(number).parser)).then((key, ctx) => ctx.success(String(key))).parser
48796
48943
  });
48797
- const parseExpressionWidget = parseWidget(constant("expression"), object({
48944
+ const parseExpressionWidgetV1 = parseWidgetWithVersion(object({
48945
+ major: constant(1),
48946
+ minor: number
48947
+ }), constant("expression"), object({
48798
48948
  answerForms: array(parseAnswerForm),
48799
48949
  functions: array(string),
48800
48950
  times: boolean,
@@ -48803,23 +48953,69 @@ const parseExpressionWidget = parseWidget(constant("expression"), object({
48803
48953
  buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48804
48954
  buttonsVisible: optional(enumeration("always", "never", "focused"))
48805
48955
  }));
48956
+ const parseExpressionWidgetV0 = parseWidgetWithVersion(optional(object({
48957
+ major: constant(0),
48958
+ minor: number
48959
+ })), constant("expression"), object({
48960
+ functions: array(string),
48961
+ times: boolean,
48962
+ visibleLabel: optional(string),
48963
+ ariaLabel: optional(string),
48964
+ form: boolean,
48965
+ simplify: boolean,
48966
+ value: string,
48967
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48968
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48969
+ }));
48970
+ function migrateV0ToV1(widget, ctx) {
48971
+ const {
48972
+ options
48973
+ } = widget;
48974
+ return ctx.success({
48975
+ ...widget,
48976
+ version: {
48977
+ major: 1,
48978
+ minor: 0
48979
+ },
48980
+ options: {
48981
+ times: options.times,
48982
+ buttonSets: options.buttonSets,
48983
+ functions: options.functions,
48984
+ buttonsVisible: options.buttonsVisible,
48985
+ visibleLabel: options.visibleLabel,
48986
+ ariaLabel: options.ariaLabel,
48987
+ answerForms: [{
48988
+ considered: "correct",
48989
+ form: options.form,
48990
+ simplify: options.simplify,
48991
+ value: options.value
48992
+ }]
48993
+ }
48994
+ });
48995
+ }
48996
+ const parseExpressionWidget = union(parseExpressionWidgetV1).or(pipeParsers(parseExpressionWidgetV0).then(migrateV0ToV1).parser).parser;
48806
48997
 
48807
- const parsePerseusRenderer = object({
48808
- content: string,
48998
+ const parsePerseusRenderer = defaulted(object({
48999
+ // TODO(benchristel): content is also defaulted to empty string in
49000
+ // renderer.tsx. See if we can remove one default or the other.
49001
+ content: defaulted(string, () => ""),
48809
49002
  // This module has an import cycle with parseWidgetsMap, because the
48810
49003
  // `group` widget can contain another renderer.
48811
49004
  // The anonymous function below ensures that we don't try to access
48812
49005
  // parseWidgetsMap before it's defined.
48813
- widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
49006
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
48814
49007
  metadata: optional(array(string)),
48815
- images: record(string, object({
48816
- width: number,
48817
- height: number
48818
- }))
48819
- });
49008
+ images: parseImages
49009
+ }),
49010
+ // Default value
49011
+ () => ({
49012
+ content: "",
49013
+ widgets: {},
49014
+ images: {}
49015
+ }));
48820
49016
 
48821
49017
  const parseGradedGroupWidgetOptions = object({
48822
- title: string,
49018
+ title: defaulted(string, () => ""),
48823
49019
  hasHint: optional(nullable(boolean)),
48824
49020
  // This module has an import cycle with parsePerseusRenderer.
48825
49021
  // The anonymous function below ensures that we don't try to access
@@ -48913,7 +49109,7 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
48913
49109
  height: union(number).or(string).parser,
48914
49110
  allowFullScreen: boolean,
48915
49111
  allowTopNavigation: optional(boolean),
48916
- static: boolean
49112
+ static: defaulted(boolean, () => false)
48917
49113
  }));
48918
49114
 
48919
49115
  const parsePerseusImageBackground = object({
@@ -49292,13 +49488,13 @@ const parseMatcherWidget = parseWidget(constant("matcher"), object({
49292
49488
  padding: boolean
49293
49489
  }));
49294
49490
 
49295
- const parseMatrixWidget = parseWidget(constant("matrix"), object({
49296
- prefix: string,
49297
- suffix: string,
49491
+ const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
49492
+ prefix: optional(string),
49493
+ suffix: optional(string),
49298
49494
  answers: array(array(number)),
49299
- cursorPosition: array(number),
49495
+ cursorPosition: optional(array(number)),
49300
49496
  matrixBoardSize: array(number),
49301
- static: boolean
49497
+ static: optional(boolean)
49302
49498
  }));
49303
49499
 
49304
49500
  const parseMeasurerWidget = parseWidget(constant("measurer"), object({
@@ -49368,8 +49564,8 @@ const parseOrdererWidget = parseWidget(constant("orderer"), object({
49368
49564
  options: array(parseRenderer),
49369
49565
  correctOptions: array(parseRenderer),
49370
49566
  otherOptions: array(parseRenderer),
49371
- height: string,
49372
- layout: string
49567
+ height: enumeration("normal", "auto"),
49568
+ layout: enumeration("horizontal", "vertical")
49373
49569
  }));
49374
49570
 
49375
49571
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49414,7 +49610,7 @@ const parsePythonProgramWidget = parseWidget(constant("python-program"), object(
49414
49610
 
49415
49611
  const parseRadioWidget = parseWidget(constant("radio"), object({
49416
49612
  choices: array(object({
49417
- content: string,
49613
+ content: defaulted(string, () => ""),
49418
49614
  clue: optional(string),
49419
49615
  correct: optional(boolean),
49420
49616
  isNoneOfTheAbove: optional(boolean),
@@ -49576,26 +49772,45 @@ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
49576
49772
  const parseHint = object({
49577
49773
  replace: optional(boolean),
49578
49774
  content: string,
49579
- widgets: parseWidgetsMap,
49775
+ widgets: defaulted(parseWidgetsMap, () => ({})),
49580
49776
  metadata: optional(array(string)),
49581
- images: record(string, object({
49582
- width: number,
49583
- height: number
49584
- }))
49777
+ images: parseImages
49585
49778
  });
49586
49779
 
49587
49780
  object({
49588
49781
  question: parsePerseusRenderer,
49589
- hints: array(parseHint),
49590
- answerArea: record(enumeration(...ItemExtras), boolean),
49591
- itemDataVersion: object({
49782
+ hints: defaulted(array(parseHint), () => []),
49783
+ answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...ItemExtras), boolean)).parser,
49784
+ itemDataVersion: optional(object({
49592
49785
  major: number,
49593
49786
  minor: number
49594
- }),
49787
+ })),
49595
49788
  // Deprecated field
49596
49789
  answer: any
49597
49790
  });
49598
49791
 
49792
+ // Some answerAreas have extra fields, like:
49793
+ //
49794
+ // "answerArea": {
49795
+ // "type": "multiple",
49796
+ // "options": {
49797
+ // "content": "",
49798
+ // "images": {},
49799
+ // "widgets": {}
49800
+ // }
49801
+ // }
49802
+ //
49803
+ // The "type" and "options" fields don't seem to be used anywhere. This
49804
+ // migration function removes them.
49805
+ function migrateAnswerArea(rawValue, ctx) {
49806
+ const {
49807
+ type: _,
49808
+ options: __,
49809
+ ...rest
49810
+ } = rawValue;
49811
+ return ctx.success(rest);
49812
+ }
49813
+
49599
49814
  /**
49600
49815
  * Helper to parse PerseusItem JSON
49601
49816
  * Why not just use JSON.parse? We want: