@khanacademy/perseus 43.0.0 → 43.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/components/number-input.d.ts +5 -3
  2. package/dist/components/text-input.d.ts +4 -2
  3. package/dist/es/index.js +475 -306
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/es/strings.js +7 -3
  6. package/dist/es/strings.js.map +1 -1
  7. package/dist/index.js +465 -296
  8. package/dist/index.js.map +1 -1
  9. package/dist/perseus-types.d.ts +14 -7
  10. package/dist/strings.js +7 -3
  11. package/dist/strings.js.map +1 -1
  12. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +1 -1
  13. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +2 -0
  14. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.d.ts +7 -0
  15. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.d.ts +1 -0
  16. package/dist/util/parse-perseus-json/general-purpose-parsers/unknown.d.ts +2 -0
  17. package/dist/util/parse-perseus-json/perseus-parsers/images-map.d.ts +5 -0
  18. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +4 -0
  19. package/dist/validation.types.d.ts +14 -6
  20. package/dist/widgets/categorizer/categorizer.d.ts +2 -2
  21. package/dist/widgets/categorizer/score-categorizer.d.ts +2 -2
  22. package/dist/widgets/categorizer/validate-categorizer.d.ts +15 -0
  23. package/dist/widgets/dropdown/validate-dropdown.d.ts +10 -0
  24. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +11 -7
  25. package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +7 -3
  26. package/dist/widgets/interactive-graphs/types.d.ts +1 -0
  27. package/dist/widgets/interactive-graphs/utils.d.ts +6 -6
  28. package/dist/widgets/matrix/matrix.d.ts +12 -5
  29. package/dist/widgets/measurer/measurer.d.ts +1 -1
  30. package/dist/widgets/orderer/validate-orderer.d.ts +12 -0
  31. package/dist/widgets/plotter/plotter.d.ts +2 -2
  32. package/dist/widgets/plotter/score-plotter.d.ts +2 -2
  33. package/dist/widgets/plotter/validate-plotter.d.ts +12 -0
  34. package/dist/widgets/radio/radio.d.ts +1 -1
  35. package/dist/widgets/radio/validate-radio.d.ts +14 -0
  36. package/dist/widgets/sorter/validate-sorter.d.ts +13 -0
  37. package/package.json +4 -4
  38. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +0 -2
package/dist/es/index.js CHANGED
@@ -23,7 +23,7 @@ import { number as number$1, point, vector as vector$1, line } from '@khanacadem
23
23
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
24
24
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
25
25
  import SimpleMarkdown from '@khanacademy/simple-markdown';
26
- import { pureMarkdownRules, parse as parse$4 } from '@khanacademy/pure-markdown';
26
+ import { pureMarkdownRules } from '@khanacademy/pure-markdown';
27
27
  import { TextField } from '@khanacademy/wonder-blocks-form';
28
28
  import Button from '@khanacademy/wonder-blocks-button';
29
29
  import { Strut } from '@khanacademy/wonder-blocks-layout';
@@ -2760,8 +2760,6 @@ async function getImageSizeModern(url) {
2760
2760
  });
2761
2761
  }
2762
2762
 
2763
- /* eslint-disable @babel/no-invalid-this, getter-return, one-var */
2764
-
2765
2763
  // TODO: dedupe this with Coord in interactive2/types.js
2766
2764
 
2767
2765
  let supportsPassive = false;
@@ -2837,8 +2835,8 @@ function shuffle$3(array, randomSeed, ensurePermuted = false) {
2837
2835
  do {
2838
2836
  // Fischer-Yates shuffle
2839
2837
  for (let top = shuffled.length; top > 0; top--) {
2840
- const newEnd = Math.floor(random() * top),
2841
- temp = shuffled[newEnd];
2838
+ const newEnd = Math.floor(random() * top);
2839
+ const temp = shuffled[newEnd];
2842
2840
 
2843
2841
  // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
2844
2842
  shuffled[newEnd] = shuffled[top - 1];
@@ -3170,13 +3168,13 @@ function eq$4(x, y) {
3170
3168
  * Deep approximate equality on primitives, numbers, arrays, and objects.
3171
3169
  * Recursive.
3172
3170
  */
3173
- function deepEq$6(x, y) {
3171
+ function deepEq$7(x, y) {
3174
3172
  if (Array.isArray(x) && Array.isArray(y)) {
3175
3173
  if (x.length !== y.length) {
3176
3174
  return false;
3177
3175
  }
3178
3176
  for (let i = 0; i < x.length; i++) {
3179
- if (!deepEq$6(x[i], y[i])) {
3177
+ if (!deepEq$7(x[i], y[i])) {
3180
3178
  return false;
3181
3179
  }
3182
3180
  }
@@ -3194,10 +3192,10 @@ function deepEq$6(x, y) {
3194
3192
  if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
3195
3193
  return x === y || _.all(x, function (v, k) {
3196
3194
  // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
3197
- return deepEq$6(y[k], v);
3195
+ return deepEq$7(y[k], v);
3198
3196
  }) && _.all(y, function (v, k) {
3199
3197
  // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
3200
- return deepEq$6(x[k], v);
3198
+ return deepEq$7(x[k], v);
3201
3199
  });
3202
3200
  }
3203
3201
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
@@ -3478,7 +3476,7 @@ const Util = {
3478
3476
  tickStepFromNumTicks,
3479
3477
  constrainedTickStepsFromTickSteps,
3480
3478
  eq: eq$4,
3481
- deepEq: deepEq$6,
3479
+ deepEq: deepEq$7,
3482
3480
  parseQueryString,
3483
3481
  updateQueryString: updateQueryString$2,
3484
3482
  strongEncodeURIComponent,
@@ -10509,7 +10507,9 @@ const getClipPoint = function getClipPoint(graph, coord, angle) {
10509
10507
  return clipPoint;
10510
10508
  };
10511
10509
 
10512
- /* eslint-disable @babel/no-invalid-this */
10510
+ /**
10511
+ * Default methods for a wrapped movable.
10512
+ */
10513
10513
 
10514
10514
  /*
10515
10515
  * attr() and animate() simply pass the
@@ -10767,7 +10767,7 @@ class WrappedLine extends WrappedDrawing {
10767
10767
  */
10768
10768
  const {
10769
10769
  eq: eq$3,
10770
- deepEq: deepEq$5
10770
+ deepEq: deepEq$6
10771
10771
  } = Util;
10772
10772
 
10773
10773
  // This should really be a readonly tuple of [number, number]
@@ -10927,7 +10927,7 @@ function similar(coords1, coords2, tolerance) {
10927
10927
  angles = rotate(angles, i);
10928
10928
  // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10929
10929
  sides = rotate(sides, i);
10930
- if (deepEq$5(angles1, angles)) {
10930
+ if (deepEq$6(angles1, angles)) {
10931
10931
  const sidePairs = _.zip(sides1, sides);
10932
10932
  const factors = _.map(sidePairs, function (pair) {
10933
10933
  return pair[0] / pair[1];
@@ -13865,7 +13865,7 @@ _.extend(MovableAngle.prototype, {
13865
13865
 
13866
13866
  /* eslint-disable @babel/no-invalid-this */
13867
13867
  const nestedMap$1 = Util.nestedMap;
13868
- const deepEq$4 = Util.deepEq;
13868
+ const deepEq$5 = Util.deepEq;
13869
13869
 
13870
13870
  /**
13871
13871
  * A base class for all Graphie Movables
@@ -13962,7 +13962,7 @@ const createSimpleClass = function createSimpleClass(addFunction) {
13962
13962
  this._prevProps = this.props;
13963
13963
  },
13964
13964
  modify: function (graphie) {
13965
- if (!deepEq$4(this.props, this._prevProps)) {
13965
+ if (!deepEq$5(this.props, this._prevProps)) {
13966
13966
  this.remove();
13967
13967
  this.add(graphie);
13968
13968
  this._prevProps = this.props;
@@ -16576,7 +16576,7 @@ var Movables = {
16576
16576
  const GraphieMovable = GraphieClasses.GraphieMovable;
16577
16577
  const createGraphie = GraphUtils.createGraphie;
16578
16578
  const {
16579
- deepEq: deepEq$3,
16579
+ deepEq: deepEq$4,
16580
16580
  nestedMap
16581
16581
  } = Util;
16582
16582
  const {
@@ -16818,7 +16818,7 @@ class Graphie extends React.Component {
16818
16818
  if (this.props.setup !== prevProps.setup) {
16819
16819
  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.", Errors.Internal);
16820
16820
  }
16821
- if (!deepEq$3(this.props.options, prevProps.options) || !deepEq$3(this.props.box, prevProps.box) || !deepEq$3(this.props.range, prevProps.range)) {
16821
+ if (!deepEq$4(this.props.options, prevProps.options) || !deepEq$4(this.props.box, prevProps.box) || !deepEq$4(this.props.range, prevProps.range)) {
16822
16822
  this._setupGraphie();
16823
16823
  }
16824
16824
  this._updateMovables();
@@ -18536,7 +18536,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18536
18536
  // The href prop should always be here, but just in case somehow the article's
18537
18537
  // link was not put together properly, let's make sure it's there so we
18538
18538
  // don't break the entire page.
18539
- const isKAUrl = href ? href.match(/https?:\/\/[^\/]*khanacademy.org|^\//) : false;
18539
+ const isKAUrl = href ? href.match(/https?:\/\/[^/]*khanacademy.org|^\//) : false;
18540
18540
  if (!isKAUrl) {
18541
18541
  // Prevents "reverse tabnabbing" phishing attacks
18542
18542
  // @ts-expect-error - TS2322 - Type '"noopener noreferrer"' is not assignable to type 'null'.
@@ -20889,36 +20889,36 @@ function uniqueIdForInput(prefix = "input-") {
20889
20889
  class TextInput extends React.Component {
20890
20890
  constructor(props) {
20891
20891
  super(props);
20892
+ this.inputRef = /*#__PURE__*/React.createRef();
20892
20893
  this.id = void 0;
20894
+ this._getInput = () => {
20895
+ if (!this.inputRef.current) {
20896
+ throw new PerseusError("Input ref accessed before set", Errors.Internal);
20897
+ }
20898
+ return this.inputRef.current;
20899
+ };
20893
20900
  this.focus = () => {
20894
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'.
20895
- ReactDOM__default.findDOMNode(this).focus();
20901
+ this._getInput().focus();
20896
20902
  };
20897
20903
  this.blur = () => {
20898
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'.
20899
- ReactDOM__default.findDOMNode(this).blur();
20904
+ this._getInput().blur();
20900
20905
  };
20901
20906
  this.getValue = () => {
20902
- var _ReactDOM$findDOMNode;
20903
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
20904
- return (_ReactDOM$findDOMNode = ReactDOM__default.findDOMNode(this)) == null ? void 0 : _ReactDOM$findDOMNode.value;
20907
+ var _this$inputRef$curren;
20908
+ return (_this$inputRef$curren = this.inputRef.current) == null ? void 0 : _this$inputRef$curren.value;
20905
20909
  };
20906
20910
  this.getStringValue = () => {
20907
- var _ReactDOM$findDOMNode2;
20908
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
20909
- return (_ReactDOM$findDOMNode2 = ReactDOM__default.findDOMNode(this)) == null ? void 0 : _ReactDOM$findDOMNode2.value.toString();
20911
+ var _this$inputRef$curren2;
20912
+ return (_this$inputRef$curren2 = this.inputRef.current) == null ? void 0 : _this$inputRef$curren2.value.toString();
20910
20913
  };
20911
20914
  this.setSelectionRange = (selectionStart, selectionEnd) => {
20912
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'.
20913
- ReactDOM__default.findDOMNode(this).setSelectionRange(selectionStart, selectionEnd);
20915
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
20914
20916
  };
20915
20917
  this.getSelectionStart = () => {
20916
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
20917
- return ReactDOM__default.findDOMNode(this).selectionStart;
20918
+ return this._getInput().selectionStart;
20918
20919
  };
20919
20920
  this.getSelectionEnd = () => {
20920
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'.
20921
- return ReactDOM__default.findDOMNode(this).selectionEnd;
20921
+ return this._getInput().selectionEnd;
20922
20922
  };
20923
20923
  if (props.id) {
20924
20924
  this.id = props.id;
@@ -20939,6 +20939,7 @@ class TextInput extends React.Component {
20939
20939
  } = this.props;
20940
20940
  const formattedValue = value === null ? "" : value.toString();
20941
20941
  return /*#__PURE__*/React.createElement(TextField, {
20942
+ ref: this.inputRef,
20942
20943
  style: style,
20943
20944
  disabled: disabled,
20944
20945
  id: this.id,
@@ -20973,7 +20974,7 @@ function _objectWithoutPropertiesLoose(r, e) {
20973
20974
  return t;
20974
20975
  }
20975
20976
 
20976
- const _excluded$5 = ["position", "visibility", "left", "top"];
20977
+ const _excluded$8 = ["position", "visibility", "left", "top"];
20977
20978
  const zIndex = 10;
20978
20979
  let HorizontalDirection = /*#__PURE__*/function (HorizontalDirection) {
20979
20980
  HorizontalDirection["Left"] = "left";
@@ -21024,7 +21025,7 @@ const TooltipArrow = _ref => {
21024
21025
  left = 100,
21025
21026
  top = 0
21026
21027
  } = _ref,
21027
- props = _objectWithoutPropertiesLoose(_ref, _excluded$5);
21028
+ props = _objectWithoutPropertiesLoose(_ref, _excluded$8);
21028
21029
  // TODO(aria): Think about adding a box-shadow to the triangle here
21029
21030
  // See http://css-tricks.com/triangle-with-shadow/
21030
21031
 
@@ -21384,7 +21385,7 @@ InputWithExamples.defaultProps = {
21384
21385
  className: ""
21385
21386
  };
21386
21387
 
21387
- const _excluded$4 = ["keypadElement", "onFocus", "value"];
21388
+ const _excluded$7 = ["keypadElement", "onFocus", "value"];
21388
21389
  class SimpleKeypadInput extends React.Component {
21389
21390
  constructor(...args) {
21390
21391
  super(...args);
@@ -21429,7 +21430,7 @@ class SimpleKeypadInput extends React.Component {
21429
21430
  onFocus,
21430
21431
  value
21431
21432
  } = _this$props,
21432
- rest = _objectWithoutPropertiesLoose(_this$props, _excluded$4);
21433
+ rest = _objectWithoutPropertiesLoose(_this$props, _excluded$7);
21433
21434
  return (
21434
21435
  /*#__PURE__*/
21435
21436
  // @ts-expect-error - TS2769 - No overload matches this call.
@@ -21991,7 +21992,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
21991
21992
  return value / 100;
21992
21993
  }
21993
21994
 
21994
- // Otherwise, we return input valuåe (number) stripped of the "%".
21995
+ // Otherwise, we return input value (number) stripped of the "%".
21995
21996
  return value;
21996
21997
  }
21997
21998
  function scoreNumericInput(userInput, rubric, strings) {
@@ -25662,14 +25663,14 @@ var Choice$1 = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.
25662
25663
  forwardedRef: ref
25663
25664
  })));
25664
25665
 
25665
- const _excluded$3 = ["content", "forwardedRef", "showContent"];
25666
+ const _excluded$6 = ["content", "forwardedRef", "showContent"];
25666
25667
  const ChoiceNoneAbove = function ChoiceNoneAbove(_ref) {
25667
25668
  let {
25668
25669
  content,
25669
25670
  forwardedRef,
25670
25671
  showContent = true
25671
25672
  } = _ref,
25672
- rest = _objectWithoutPropertiesLoose(_ref, _excluded$3);
25673
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded$6);
25673
25674
  const {
25674
25675
  strings
25675
25676
  } = usePerseusI18n();
@@ -26040,7 +26041,15 @@ const styles$p = StyleSheet.create({
26040
26041
  }
26041
26042
  });
26042
26043
 
26043
- function scoreRadio(userInput, rubric, strings) {
26044
+ /**
26045
+ * Checks if the user has selected at least one option. Additional validation
26046
+ * is done in scoreRadio to check if the number of selected options is correct
26047
+ * and if the user has selected both a correct option and the "none of the above"
26048
+ * option.
26049
+ * @param userInput
26050
+ * @see `scoreRadio` for the additional validation logic and the scoring logic.
26051
+ */
26052
+ function validateRadio(userInput) {
26044
26053
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26045
26054
  return sum + (selected ? 1 : 0);
26046
26055
  }, 0);
@@ -26050,6 +26059,17 @@ function scoreRadio(userInput, rubric, strings) {
26050
26059
  message: null
26051
26060
  };
26052
26061
  }
26062
+ return null;
26063
+ }
26064
+
26065
+ function scoreRadio(userInput, rubric, strings) {
26066
+ const validationError = validateRadio(userInput);
26067
+ if (validationError) {
26068
+ return validationError;
26069
+ }
26070
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26071
+ return sum + (selected ? 1 : 0);
26072
+ }, 0);
26053
26073
  const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
26054
26074
  return currentChoice.correct ? sum + 1 : sum;
26055
26075
  }, 0);
@@ -26426,6 +26446,7 @@ Radio$1.defaultProps = {
26426
26446
  showSolutions: "none"
26427
26447
  };
26428
26448
 
26449
+ const _excluded$5 = ["noneOfTheAbove"];
26429
26450
  const {
26430
26451
  shuffle: shuffle$2,
26431
26452
  random
@@ -26514,17 +26535,15 @@ const transform = (widgetOptions, strings, problemNum) => {
26514
26535
  };
26515
26536
  const propUpgrades$1 = {
26516
26537
  "1": v0props => {
26517
- let choices;
26518
- let hasNoneOfTheAbove;
26519
- if (!v0props.noneOfTheAbove) {
26520
- choices = v0props.choices;
26521
- hasNoneOfTheAbove = false;
26522
- } else {
26538
+ const {
26539
+ noneOfTheAbove
26540
+ } = v0props,
26541
+ rest = _objectWithoutPropertiesLoose(v0props, _excluded$5);
26542
+ if (noneOfTheAbove) {
26523
26543
  throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
26524
26544
  }
26525
- return _.extend(_.omit(v0props, "noneOfTheAbove"), {
26526
- choices: choices,
26527
- hasNoneOfTheAbove: hasNoneOfTheAbove
26545
+ return _extends({}, rest, {
26546
+ hasNoneOfTheAbove: false
26528
26547
  });
26529
26548
  }
26530
26549
  };
@@ -26562,23 +26581,36 @@ const getPromptJSON$n = (renderProps, userInput) => {
26562
26581
  };
26563
26582
  };
26564
26583
 
26565
- function scoreCategorizer(userInput, rubric, strings) {
26566
- let completed = true;
26567
- let allCorrect = true;
26568
- rubric.values.forEach((value, i) => {
26569
- if (userInput.values[i] == null) {
26570
- completed = false;
26571
- }
26572
- if (userInput.values[i] !== value) {
26573
- allCorrect = false;
26574
- }
26575
- });
26576
- if (!completed) {
26584
+ /**
26585
+ * Checks userInput from the categorizer widget to see if the user has selected
26586
+ * a category for each item.
26587
+ * @param userInput - The user's input corresponding to an array of indices that
26588
+ * represent the selected category for each row/item.
26589
+ * @param validationData - An array of strings corresponding to each row/item
26590
+ * @param strings - Used to provide a validation message
26591
+ */
26592
+ function validateCategorizer(userInput, validationData, strings) {
26593
+ const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
26594
+ if (incomplete) {
26577
26595
  return {
26578
26596
  type: "invalid",
26579
26597
  message: strings.invalidSelection
26580
26598
  };
26581
26599
  }
26600
+ return null;
26601
+ }
26602
+
26603
+ function scoreCategorizer(userInput, scoringData, strings) {
26604
+ const validationError = validateCategorizer(userInput, scoringData, strings);
26605
+ if (validationError) {
26606
+ return validationError;
26607
+ }
26608
+ let allCorrect = true;
26609
+ scoringData.values.forEach((value, i) => {
26610
+ if (userInput.values[i] !== value) {
26611
+ allCorrect = false;
26612
+ }
26613
+ });
26582
26614
  return {
26583
26615
  type: "points",
26584
26616
  earned: allCorrect ? 1 : 0,
@@ -27124,15 +27156,26 @@ const getPromptJSON$k = (renderProps, userInput) => {
27124
27156
  };
27125
27157
  };
27126
27158
 
27127
- function scoreDropdown(userInput, rubric) {
27128
- const selected = userInput.value;
27129
- if (selected === 0) {
27159
+ /**
27160
+ * Checks if the user has selected an item from the dropdown before scoring.
27161
+ * This is shown with a userInput value / index other than 0.
27162
+ */
27163
+ function validateDropdown(userInput) {
27164
+ if (userInput.value === 0) {
27130
27165
  return {
27131
27166
  type: "invalid",
27132
27167
  message: null
27133
27168
  };
27134
27169
  }
27135
- const correct = rubric.choices[selected - 1].correct;
27170
+ return null;
27171
+ }
27172
+
27173
+ function scoreDropdown(userInput, rubric) {
27174
+ const validationError = validateDropdown(userInput);
27175
+ if (validationError) {
27176
+ return validationError;
27177
+ }
27178
+ const correct = rubric.choices[userInput.value - 1].correct;
27136
27179
  return {
27137
27180
  type: "points",
27138
27181
  earned: correct ? 1 : 0,
@@ -27519,8 +27562,6 @@ const getNextState = (currentState, answerable) => {
27519
27562
  }
27520
27563
  };
27521
27564
 
27522
- // exports has no 'transform'
27523
-
27524
27565
  // A Graded Group is more or less a Group widget that displays a check
27525
27566
  // answer button below the rendered content. When clicked, the widget grades
27526
27567
  // the stuff inside and displays feedback about whether the inputted answer was
@@ -28184,7 +28225,6 @@ const getPromptJSON$g = (renderProps, userInput) => {
28184
28225
  };
28185
28226
  };
28186
28227
 
28187
- /* eslint-disable one-var */
28188
28228
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
28189
28229
  const Plot$1 = Graphie.Plot;
28190
28230
  const DEFAULT_BACKGROUND_IMAGE = {
@@ -28285,14 +28325,14 @@ const Linear = _.extend({}, PlotDefaults, {
28285
28325
  return [m, b];
28286
28326
  },
28287
28327
  getFunctionForCoeffs: function (coeffs, x) {
28288
- const m = coeffs[0],
28289
- b = coeffs[1];
28328
+ const m = coeffs[0];
28329
+ const b = coeffs[1];
28290
28330
  return m * x + b;
28291
28331
  },
28292
28332
  getEquationString: function (coords) {
28293
28333
  const coeffs = this.getCoefficients(coords);
28294
- const m = coeffs[0],
28295
- b = coeffs[1];
28334
+ const m = coeffs[0];
28335
+ const b = coeffs[1];
28296
28336
  return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
28297
28337
  }
28298
28338
  });
@@ -28316,9 +28356,9 @@ const Quadratic = _.extend({}, PlotDefaults, {
28316
28356
  return [a, b, c];
28317
28357
  },
28318
28358
  getFunctionForCoeffs: function (coeffs, x) {
28319
- const a = coeffs[0],
28320
- b = coeffs[1],
28321
- c = coeffs[2];
28359
+ const a = coeffs[0];
28360
+ const b = coeffs[1];
28361
+ const c = coeffs[2];
28322
28362
  return (a * x + b) * x + c;
28323
28363
  },
28324
28364
  getPropsForCoeffs: function (coeffs) {
@@ -28330,9 +28370,9 @@ const Quadratic = _.extend({}, PlotDefaults, {
28330
28370
  },
28331
28371
  getEquationString: function (coords) {
28332
28372
  const coeffs = this.getCoefficients(coords);
28333
- const a = coeffs[0],
28334
- b = coeffs[1],
28335
- c = coeffs[2];
28373
+ const a = coeffs[0];
28374
+ const b = coeffs[1];
28375
+ const c = coeffs[2];
28336
28376
  return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
28337
28377
  }
28338
28378
  });
@@ -28351,10 +28391,10 @@ const Sinusoid = _.extend({}, PlotDefaults, {
28351
28391
  return [a, b, c, d];
28352
28392
  },
28353
28393
  getFunctionForCoeffs: function (coeffs, x) {
28354
- const a = coeffs[0],
28355
- b = coeffs[1],
28356
- c = coeffs[2],
28357
- d = coeffs[3];
28394
+ const a = coeffs[0];
28395
+ const b = coeffs[1];
28396
+ const c = coeffs[2];
28397
+ const d = coeffs[3];
28358
28398
  return a * Math.sin(b * x - c) + d;
28359
28399
  },
28360
28400
  getPropsForCoeffs: function (coeffs) {
@@ -28367,10 +28407,10 @@ const Sinusoid = _.extend({}, PlotDefaults, {
28367
28407
  },
28368
28408
  getEquationString: function (coords) {
28369
28409
  const coeffs = this.getCoefficients(coords);
28370
- const a = coeffs[0],
28371
- b = coeffs[1],
28372
- c = coeffs[2],
28373
- d = coeffs[3];
28410
+ const a = coeffs[0];
28411
+ const b = coeffs[1];
28412
+ const c = coeffs[2];
28413
+ const d = coeffs[3];
28374
28414
  return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28375
28415
  },
28376
28416
  areEqual: function (coeffs1, coeffs2) {
@@ -28390,18 +28430,18 @@ const Tangent = _.extend({}, PlotDefaults, {
28390
28430
  return [a, b, c, d];
28391
28431
  },
28392
28432
  getFunctionForCoeffs: function (coeffs, x) {
28393
- const a = coeffs[0],
28394
- b = coeffs[1],
28395
- c = coeffs[2],
28396
- d = coeffs[3];
28433
+ const a = coeffs[0];
28434
+ const b = coeffs[1];
28435
+ const c = coeffs[2];
28436
+ const d = coeffs[3];
28397
28437
  return a * Math.tan(b * x - c) + d;
28398
28438
  },
28399
28439
  getEquationString: function (coords) {
28400
28440
  const coeffs = this.getCoefficients(coords);
28401
- const a = coeffs[0],
28402
- b = coeffs[1],
28403
- c = coeffs[2],
28404
- d = coeffs[3];
28441
+ const a = coeffs[0];
28442
+ const b = coeffs[1];
28443
+ const c = coeffs[2];
28444
+ const d = coeffs[3];
28405
28445
  return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28406
28446
  },
28407
28447
  areEqual: function (coeffs1, coeffs2) {
@@ -28460,9 +28500,9 @@ const Exponential = _.extend({}, PlotDefaults, {
28460
28500
  return [a, b, c];
28461
28501
  },
28462
28502
  getFunctionForCoeffs: function (coeffs, x) {
28463
- const a = coeffs[0],
28464
- b = coeffs[1],
28465
- c = coeffs[2];
28503
+ const a = coeffs[0];
28504
+ const b = coeffs[1];
28505
+ const c = coeffs[2];
28466
28506
  return a * Math.exp(b * x) + c;
28467
28507
  },
28468
28508
  getEquationString: function (coords, asymptote) {
@@ -28470,9 +28510,9 @@ const Exponential = _.extend({}, PlotDefaults, {
28470
28510
  return null;
28471
28511
  }
28472
28512
  const coeffs = this.getCoefficients(coords, asymptote);
28473
- const a = coeffs[0],
28474
- b = coeffs[1],
28475
- c = coeffs[2];
28513
+ const a = coeffs[0];
28514
+ const b = coeffs[1];
28515
+ const c = coeffs[2];
28476
28516
  return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
28477
28517
  }
28478
28518
  });
@@ -28517,9 +28557,9 @@ const Logarithm = _.extend({}, PlotDefaults, {
28517
28557
  }
28518
28558
  },
28519
28559
  getFunctionForCoeffs: function (coeffs, x, asymptote) {
28520
- const a = coeffs[0],
28521
- b = coeffs[1],
28522
- c = coeffs[2];
28560
+ const a = coeffs[0];
28561
+ const b = coeffs[1];
28562
+ const c = coeffs[2];
28523
28563
  return a * Math.log(b * x + c);
28524
28564
  },
28525
28565
  getEquationString: function (coords, asymptote) {
@@ -28527,9 +28567,9 @@ const Logarithm = _.extend({}, PlotDefaults, {
28527
28567
  return null;
28528
28568
  }
28529
28569
  const coeffs = this.getCoefficients(coords, asymptote);
28530
- const a = coeffs[0],
28531
- b = coeffs[1],
28532
- c = coeffs[2];
28570
+ const a = coeffs[0];
28571
+ const b = coeffs[1];
28572
+ const c = coeffs[2];
28533
28573
  return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
28534
28574
  }
28535
28575
  });
@@ -28553,16 +28593,16 @@ const AbsoluteValue = _.extend({}, PlotDefaults, {
28553
28593
  return [m, horizontalOffset, verticalOffset];
28554
28594
  },
28555
28595
  getFunctionForCoeffs: function (coeffs, x) {
28556
- const m = coeffs[0],
28557
- horizontalOffset = coeffs[1],
28558
- verticalOffset = coeffs[2];
28596
+ const m = coeffs[0];
28597
+ const horizontalOffset = coeffs[1];
28598
+ const verticalOffset = coeffs[2];
28559
28599
  return m * Math.abs(x - horizontalOffset) + verticalOffset;
28560
28600
  },
28561
28601
  getEquationString: function (coords) {
28562
28602
  const coeffs = this.getCoefficients(coords);
28563
- const m = coeffs[0],
28564
- horizontalOffset = coeffs[1],
28565
- verticalOffset = coeffs[2];
28603
+ const m = coeffs[0];
28604
+ const horizontalOffset = coeffs[1];
28605
+ const verticalOffset = coeffs[2];
28566
28606
  return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
28567
28607
  }
28568
28608
  });
@@ -31129,67 +31169,6 @@ function isUnlimitedGraphState(state) {
31129
31169
  return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31130
31170
  }
31131
31171
 
31132
- /**
31133
- * Replace all text outside of the $ TeX blocks with `\\text{...}`
31134
- * This way, the entire resulting string can be rendered within <TeX>
31135
- * and the text outside of the $ blocks will be non-TeX text.
31136
- */
31137
- function replaceOutsideTeX(mathString) {
31138
- // All the information we need is in the first section,
31139
- // whether it's typed as "blockmath" or "paragraph"
31140
- const firstSection = parse$4(mathString)[0];
31141
-
31142
- // If it's blockMath, the outer level has the full math content.
31143
- if (firstSection.type === "blockMath") {
31144
- return firstSection.content;
31145
- }
31146
-
31147
- // If it's a paragraph, we need to iterate through the sections
31148
- // to look for individual math blocks.
31149
- const condensedNodes = condenseTextNodes(firstSection.content);
31150
- let result = "";
31151
- for (const piece of condensedNodes) {
31152
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31153
- }
31154
- return result;
31155
- }
31156
- // Helper function for replaceOutsideTeX()
31157
- // Condense adjacent text nodes into a single text node
31158
- function condenseTextNodes(nodes) {
31159
- const result = [];
31160
- if (!nodes) {
31161
- return result;
31162
- }
31163
- let currentText = "";
31164
- for (const node of nodes) {
31165
- if (node.type === "math") {
31166
- if (currentText) {
31167
- result.push({
31168
- type: "text",
31169
- content: currentText
31170
- });
31171
- currentText = "";
31172
- }
31173
- result.push(node);
31174
- } else {
31175
- currentText += node.content;
31176
- }
31177
- }
31178
- if (currentText) {
31179
- result.push({
31180
- type: "text",
31181
- content: currentText
31182
- });
31183
- }
31184
- return result;
31185
- }
31186
-
31187
- // Helper function for replaceOutsideTeX()
31188
- function escapeSpecialChars(str) {
31189
- // Escape $, \, {, and } characters
31190
- return str.replace(/([$\\{}])/g, "\\$1");
31191
- }
31192
-
31193
31172
  /**
31194
31173
  * Parse a string of text and math into a list of objects with type and content
31195
31174
  *
@@ -31221,6 +31200,29 @@ const mathOnlyParser = SimpleMarkdown.parserFor({
31221
31200
  inline: true
31222
31201
  });
31223
31202
 
31203
+ /**
31204
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
31205
+ * This way, the entire resulting string can be rendered within <TeX>
31206
+ * and the text outside of the $ blocks will be non-TeX text.
31207
+ */
31208
+ function replaceOutsideTeX(mathString) {
31209
+ // All the information we need is in the first section,
31210
+ // whether it's typed as "blockmath" or "paragraph"
31211
+ const parsed = mathOnlyParser(mathString);
31212
+ let result = "";
31213
+ for (const piece of parsed) {
31214
+ piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
31215
+ }
31216
+ return `\\text{${result}}`;
31217
+ }
31218
+ function escapeIfUnescaped(character) {
31219
+ if (character.length === 1) {
31220
+ return "\\" + character;
31221
+ } else {
31222
+ return character;
31223
+ }
31224
+ }
31225
+
31224
31226
  function AxisLabels() {
31225
31227
  const {
31226
31228
  range,
@@ -31764,7 +31766,7 @@ const styles$e = StyleSheet.create({
31764
31766
  }
31765
31767
  });
31766
31768
 
31767
- const _excluded$2 = ["onFormatChange", "checkValidity", "useArrowKeys"];
31769
+ const _excluded$4 = ["onFormatChange", "checkValidity", "useArrowKeys"];
31768
31770
  const {
31769
31771
  firstNumericalParse,
31770
31772
  captureScratchpadTouchStart
@@ -31794,22 +31796,25 @@ const getNumericFormat = KhanMath.getNumericFormat;
31794
31796
  class NumberInput extends React.Component {
31795
31797
  constructor(...args) {
31796
31798
  super(...args);
31799
+ this.inputRef = /*#__PURE__*/React.createRef();
31797
31800
  this.state = {
31798
31801
  format: this.props.format
31799
31802
  };
31803
+ this._getInput = () => {
31804
+ if (!this.inputRef.current) {
31805
+ throw new PerseusError("Input ref accessed before set", Errors.Internal);
31806
+ }
31807
+ return this.inputRef.current;
31808
+ };
31800
31809
  /* Return the current "value" of this input
31801
31810
  * If empty, it returns the placeholder (if it is a number) or null
31802
31811
  */
31803
31812
  this.getValue = () => {
31804
- return this.parseInputValue(
31805
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
31806
- ReactDOM__default.findDOMNode(this.refs.input).value // eslint-disable-line react/no-string-refs
31807
- );
31813
+ return this.parseInputValue(this._getInput().value);
31808
31814
  };
31809
31815
  /* Return the current string value of this input */
31810
31816
  this.getStringValue = () => {
31811
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
31812
- return ReactDOM__default.findDOMNode(this.refs.input).value.toString(); // eslint-disable-line react/no-string-refs
31817
+ return this._getInput().toString();
31813
31818
  };
31814
31819
  this.parseInputValue = value => {
31815
31820
  if (value === "") {
@@ -31821,26 +31826,21 @@ class NumberInput extends React.Component {
31821
31826
  };
31822
31827
  /* Set text input focus to this input */
31823
31828
  this.focus = () => {
31824
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'.
31825
- ReactDOM__default.findDOMNode(this.refs.input).focus(); // eslint-disable-line react/no-string-refs
31829
+ this._getInput().focus();
31826
31830
  this._handleFocus();
31827
31831
  };
31828
31832
  this.blur = () => {
31829
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'.
31830
- ReactDOM__default.findDOMNode(this.refs.input).blur(); // eslint-disable-line react/no-string-refs
31833
+ this._getInput().blur();
31831
31834
  this._handleBlur();
31832
31835
  };
31833
31836
  this.setSelectionRange = (selectionStart, selectionEnd) => {
31834
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'.
31835
- ReactDOM__default.findDOMNode(this).setSelectionRange(selectionStart, selectionEnd);
31837
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
31836
31838
  };
31837
31839
  this.getSelectionStart = () => {
31838
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
31839
- return ReactDOM__default.findDOMNode(this).selectionStart;
31840
+ return this._getInput().selectionStart;
31840
31841
  };
31841
31842
  this.getSelectionEnd = () => {
31842
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'.
31843
- return ReactDOM__default.findDOMNode(this).selectionEnd;
31843
+ return this._getInput().selectionEnd;
31844
31844
  };
31845
31845
  this._checkValidity = value => {
31846
31846
  if (value == null) {
@@ -31900,9 +31900,7 @@ class NumberInput extends React.Component {
31900
31900
  }
31901
31901
  };
31902
31902
  this._setValue = (val, format) => {
31903
- // eslint-disable-next-line react/no-string-refs
31904
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'val' does not exist on type 'JQueryStatic'.
31905
- $(ReactDOM__default.findDOMNode(this.refs.input)).val(toNumericString(val, format));
31903
+ this._getInput().value = toNumericString(val, format);
31906
31904
  };
31907
31905
  }
31908
31906
  componentDidUpdate(prevProps) {
@@ -31922,13 +31920,11 @@ class NumberInput extends React.Component {
31922
31920
  classes = classes + " " + this.props.className;
31923
31921
  }
31924
31922
  const _this$props = this.props,
31925
- restProps = _objectWithoutPropertiesLoose(_this$props, _excluded$2);
31923
+ restProps = _objectWithoutPropertiesLoose(_this$props, _excluded$4);
31926
31924
  return /*#__PURE__*/React.createElement("input", _extends({}, restProps, {
31927
31925
  className: classes,
31928
- type: "text"
31929
- // eslint-disable-next-line react/no-string-refs
31930
- ,
31931
- ref: "input",
31926
+ type: "text",
31927
+ ref: this.inputRef,
31932
31928
  onChange: this._handleChange,
31933
31929
  onFocus: this._handleFocus,
31934
31930
  onBlur: this._handleBlur,
@@ -32849,12 +32845,12 @@ const MafsCssTransformWrapper = ({
32849
32845
  }
32850
32846
  }, children);
32851
32847
 
32852
- const _excluded$1 = ["children"];
32848
+ const _excluded$3 = ["children"];
32853
32849
  const TextLabel = _ref => {
32854
32850
  let {
32855
32851
  children
32856
32852
  } = _ref,
32857
- rest = _objectWithoutPropertiesLoose(_ref, _excluded$1);
32853
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded$3);
32858
32854
  return /*#__PURE__*/React__default.createElement(Text$1, _extends({
32859
32855
  size: 16,
32860
32856
  svgTextProps: {
@@ -33435,15 +33431,18 @@ function useControlPoint(params) {
33435
33431
  } = useGraphConfig();
33436
33432
  const {
33437
33433
  point,
33438
- sequenceNumber,
33434
+ ariaDescribedBy,
33435
+ ariaLabel,
33436
+ ariaLive = "polite",
33439
33437
  color,
33440
- cursor,
33441
33438
  constrain = p => snap(snapStep, p),
33439
+ cursor,
33440
+ forwardedRef = noop$1,
33441
+ sequenceNumber = 1,
33442
33442
  onMove = noop$1,
33443
33443
  onClick = noop$1,
33444
33444
  onFocus = noop$1,
33445
- onBlur = noop$1,
33446
- forwardedRef = noop$1
33445
+ onBlur = noop$1
33447
33446
  } = params;
33448
33447
  const {
33449
33448
  strings,
@@ -33466,6 +33465,13 @@ function useControlPoint(params) {
33466
33465
  onMove,
33467
33466
  constrainKeyboardMovement: constrain
33468
33467
  });
33468
+
33469
+ // if custom aria label is not provided, will use default of sequence number and point coordinates
33470
+ const pointAriaLabel = ariaLabel || strings.srPointAtCoordinates({
33471
+ num: sequenceNumber,
33472
+ x: srFormatNumber(point[X], locale),
33473
+ y: srFormatNumber(point[Y], locale)
33474
+ });
33469
33475
  useLayoutEffect(() => {
33470
33476
  setForwardedRef(forwardedRef, focusableHandleRef.current);
33471
33477
  }, [forwardedRef]);
@@ -33475,15 +33481,9 @@ function useControlPoint(params) {
33475
33481
  tabIndex: disableKeyboardInteraction ? -1 : 0,
33476
33482
  ref: focusableHandleRef,
33477
33483
  role: "button",
33478
- "aria-label": strings.srPointAtCoordinates({
33479
- num: sequenceNumber,
33480
- x: srFormatNumber(point[X], locale),
33481
- y: srFormatNumber(point[Y], locale)
33482
- })
33483
- // aria-live="assertive" causes the new location of the point to be
33484
- // announced immediately on move.
33485
- ,
33486
- "aria-live": "assertive",
33484
+ "aria-describedby": ariaDescribedBy,
33485
+ "aria-label": pointAriaLabel,
33486
+ "aria-live": ariaLive,
33487
33487
  onFocus: event => {
33488
33488
  onFocus(event);
33489
33489
  setFocused(true);
@@ -34769,6 +34769,7 @@ const MafsGraph = props => {
34769
34769
  const uniqueId = React.useId();
34770
34770
  const descriptionId = `interactive-graph-description-${uniqueId}`;
34771
34771
  const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
34772
+ const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
34772
34773
  const graphRef = React.useRef(null);
34773
34774
  const {
34774
34775
  analytics
@@ -34841,7 +34842,7 @@ const MafsGraph = props => {
34841
34842
  handleKeyboardEvent(event, state, dispatch);
34842
34843
  },
34843
34844
  "aria-label": fullGraphAriaLabel,
34844
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
34845
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
34845
34846
  ref: graphRef,
34846
34847
  tabIndex: 0,
34847
34848
  onFocus: event => {
@@ -34911,6 +34912,7 @@ const MafsGraph = props => {
34911
34912
  height: height
34912
34913
  }, /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React.createElement(View, {
34913
34914
  style: {
34915
+ display: interactionPrompt ? undefined : "hidden",
34914
34916
  textAlign: "center",
34915
34917
  backgroundColor: "white",
34916
34918
  border: "1px solid #21242C52",
@@ -34922,7 +34924,9 @@ const MafsGraph = props => {
34922
34924
  top: "50%",
34923
34925
  transform: "translateY(-50%)"
34924
34926
  }
34925
- }, /*#__PURE__*/React.createElement(LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
34927
+ }, /*#__PURE__*/React.createElement(LabelMedium, {
34928
+ id: unlimitedGraphKeyboardPromptId
34929
+ }, strings.graphKeyboardPrompt))), renderGraphControls({
34926
34930
  state,
34927
34931
  dispatch,
34928
34932
  width,
@@ -34985,6 +34989,10 @@ const renderPolygonGraphControls = props => {
34985
34989
  } = props;
34986
34990
  const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
34987
34991
 
34992
+ // We want to disable the closePolygon button when
34993
+ // there are not enough coords to make a polygon
34994
+ const disableCloseButton = coords.length < 3;
34995
+
34988
34996
  // If polygon is closed, show open button.
34989
34997
  // If polygon is open, show close button.
34990
34998
  const polygonButton = closedPolygon ? /*#__PURE__*/React.createElement(Button, {
@@ -35002,12 +35010,12 @@ const renderPolygonGraphControls = props => {
35002
35010
  // Conditional disable when there are less than 3 points in
35003
35011
  // the graph
35004
35012
  ,
35005
- disabled: coords.length < 3,
35013
+ disabled: disableCloseButton,
35006
35014
  style: {
35007
35015
  width: "100%",
35008
35016
  marginLeft: "20px"
35009
35017
  },
35010
- tabIndex: 0,
35018
+ tabIndex: disableCloseButton ? -1 : 0,
35011
35019
  onClick: () => {
35012
35020
  props.dispatch(actions.polygon.closePolygon());
35013
35021
  }
@@ -35895,8 +35903,12 @@ function doMovePoint(state, action) {
35895
35903
  newValue: newValue
35896
35904
  });
35897
35905
 
35898
- // Reject the move if it would cause the sides of the polygon to cross
35899
- if (polygonSidesIntersect(newCoords)) {
35906
+ // Boolean value to track whether we can let the polygon sides interact.
35907
+ // They can interact if it's an unlimited polygon that is open.
35908
+ const polygonSidesCanIntersect = state.numSides === "unlimited" && !state.closedPolygon;
35909
+
35910
+ // Reject the move if it would cause the sides of the polygon to cross.
35911
+ if (!polygonSidesCanIntersect && polygonSidesIntersect(newCoords)) {
35900
35912
  return state;
35901
35913
  }
35902
35914
  return _extends({}, state, {
@@ -36477,7 +36489,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
36477
36489
  });
36478
36490
 
36479
36491
  const eq$1 = Util.eq;
36480
- const deepEq$2 = Util.deepEq;
36492
+ const deepEq$3 = Util.deepEq;
36481
36493
  function scoreInteractiveGraph(userInput, rubric) {
36482
36494
  // None-type graphs are not graded
36483
36495
  if (userInput.type === "none" && rubric.correct.type === "none") {
@@ -36527,7 +36539,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36527
36539
  // If the parabola coefficients match, it's correct.
36528
36540
  const guessCoeffs = getQuadraticCoefficients(userInput.coords);
36529
36541
  const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
36530
- if (deepEq$2(guessCoeffs, correctCoeffs)) {
36542
+ if (deepEq$3(guessCoeffs, correctCoeffs)) {
36531
36543
  return {
36532
36544
  type: "points",
36533
36545
  earned: 1,
@@ -36541,7 +36553,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36541
36553
  const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
36542
36554
  const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
36543
36555
  // If the canonical coefficients match, it's correct.
36544
- if (deepEq$2(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36556
+ if (deepEq$3(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36545
36557
  return {
36546
36558
  type: "points",
36547
36559
  earned: 1,
@@ -36550,7 +36562,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36550
36562
  };
36551
36563
  }
36552
36564
  } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
36553
- if (deepEq$2(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36565
+ if (deepEq$3(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36554
36566
  return {
36555
36567
  type: "points",
36556
36568
  earned: 1,
@@ -36572,7 +36584,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36572
36584
  guess == null || guess.sort();
36573
36585
  // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
36574
36586
  correct.sort();
36575
- if (deepEq$2(guess, correct)) {
36587
+ if (deepEq$3(guess, correct)) {
36576
36588
  return {
36577
36589
  type: "points",
36578
36590
  earned: 1,
@@ -36594,7 +36606,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36594
36606
  /* exact */
36595
36607
  guess.sort();
36596
36608
  correct.sort();
36597
- match = deepEq$2(guess, correct);
36609
+ match = deepEq$3(guess, correct);
36598
36610
  }
36599
36611
  if (match) {
36600
36612
  return {
@@ -36609,7 +36621,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36609
36621
  let correct = Util.deepClone(rubric.correct.coords);
36610
36622
  guess = _.invoke(guess, "sort").sort();
36611
36623
  correct = _.invoke(correct, "sort").sort();
36612
- if (deepEq$2(guess, correct)) {
36624
+ if (deepEq$3(guess, correct)) {
36613
36625
  return {
36614
36626
  type: "points",
36615
36627
  earned: 1,
@@ -36620,7 +36632,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36620
36632
  } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
36621
36633
  const guess = userInput.coords;
36622
36634
  const correct = rubric.correct.coords;
36623
- if (deepEq$2(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
36635
+ if (deepEq$3(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
36624
36636
  return {
36625
36637
  type: "points",
36626
36638
  earned: 1,
@@ -36647,7 +36659,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36647
36659
  /* exact */
36648
36660
  match =
36649
36661
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36650
- deepEq$2(guess[1], correct[1]) &&
36662
+ deepEq$3(guess[1], correct[1]) &&
36651
36663
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36652
36664
  collinear(correct[1], correct[0], guess[0]) &&
36653
36665
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
@@ -40821,6 +40833,7 @@ const getPromptJSON$7 = () => {
40821
40833
  return getUnsupportedPromptJSON("measurer");
40822
40834
  };
40823
40835
 
40836
+ const _excluded$2 = ["imageUrl", "imageTop", "imageLeft"];
40824
40837
  const defaultImage = {
40825
40838
  url: null,
40826
40839
  top: 0,
@@ -40927,14 +40940,19 @@ Measurer.defaultProps = {
40927
40940
  };
40928
40941
  const propUpgrades = {
40929
40942
  "1": v0props => {
40930
- const v1props = _(v0props).chain().omit("imageUrl", "imageTop", "imageLeft").extend({
40943
+ const {
40944
+ imageUrl,
40945
+ imageTop,
40946
+ imageLeft
40947
+ } = v0props,
40948
+ rest = _objectWithoutPropertiesLoose(v0props, _excluded$2);
40949
+ return _extends({}, rest, {
40931
40950
  image: {
40932
- url: v0props.imageUrl,
40933
- top: v0props.imageTop,
40934
- left: v0props.imageLeft
40951
+ url: imageUrl,
40952
+ top: imageTop,
40953
+ left: imageLeft
40935
40954
  }
40936
- }).value();
40937
- return v1props;
40955
+ });
40938
40956
  }
40939
40957
  };
40940
40958
  var Measurer$1 = {
@@ -42462,14 +42480,28 @@ const getPromptJSON$5 = (renderProps, userInput) => {
42462
42480
  };
42463
42481
  };
42464
42482
 
42465
- function scoreOrderer(userInput, rubric) {
42483
+ /**
42484
+ * Checks user input from the orderer widget to see if the user has started
42485
+ * ordering the options, making the widget scorable.
42486
+ * @param userInput
42487
+ * @see `scoreOrderer` for more details.
42488
+ */
42489
+ function validateOrderer(userInput) {
42466
42490
  if (userInput.current.length === 0) {
42467
42491
  return {
42468
42492
  type: "invalid",
42469
42493
  message: null
42470
42494
  };
42471
42495
  }
42472
- const correct = _.isEqual(userInput.current, _.pluck(rubric.correctOptions, "content"));
42496
+ return null;
42497
+ }
42498
+
42499
+ function scoreOrderer(userInput, rubric) {
42500
+ const validateError = validateOrderer(userInput);
42501
+ if (validateError) {
42502
+ return validateError;
42503
+ }
42504
+ const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
42473
42505
  return {
42474
42506
  type: "points",
42475
42507
  earned: correct ? 1 : 0,
@@ -42649,8 +42681,6 @@ Card.defaultProps = {
42649
42681
  animating: false,
42650
42682
  linterContext: linterContextDefault
42651
42683
  };
42652
- const NORMAL = "normal";
42653
- const HORIZONTAL = "horizontal";
42654
42684
  class Orderer extends React.Component {
42655
42685
  constructor(...args) {
42656
42686
  super(...args);
@@ -42797,7 +42827,7 @@ class Orderer extends React.Component {
42797
42827
  };
42798
42828
  this.findCorrectIndex = (draggable, list) => {
42799
42829
  // Find the correct index for a card given the current cards.
42800
- const isHorizontal = this.props.layout === HORIZONTAL;
42830
+ const isHorizontal = this.props.layout === "horizontal";
42801
42831
  // eslint-disable-next-line react/no-string-refs
42802
42832
  // @ts-expect-error - TS2769 - No overload matches this call.
42803
42833
  const $dragList = $(ReactDOM__default.findDOMNode(this.refs.dragList));
@@ -42847,28 +42877,24 @@ class Orderer extends React.Component {
42847
42877
  if (draggable == null) {
42848
42878
  return false;
42849
42879
  }
42850
- const isHorizontal = this.props.layout === HORIZONTAL,
42851
- // @ts-expect-error - TS2769 - No overload matches this call.
42852
- $draggable = $(ReactDOM__default.findDOMNode(draggable)),
42853
- // eslint-disable-next-line react/no-string-refs
42854
- // @ts-expect-error - TS2769 - No overload matches this call.
42855
- $bank = $(ReactDOM__default.findDOMNode(this.refs.bank)),
42856
- // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
42857
- draggableOffset = $draggable.offset(),
42858
- // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
42859
- bankOffset = $bank.offset(),
42860
- // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
42861
- draggableHeight = $draggable.outerHeight(true),
42862
- // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
42863
- bankHeight = $bank.outerHeight(true),
42864
- // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
42865
- bankWidth = $bank.outerWidth(true),
42866
- // eslint-disable-next-line react/no-string-refs
42867
- dragList = ReactDOM__default.findDOMNode(this.refs.dragList);
42868
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'width' does not exist on type 'JQueryStatic'.
42869
- $(dragList).width();
42870
- const // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
42871
- draggableWidth = $draggable.outerWidth(true);
42880
+ const isHorizontal = this.props.layout === "horizontal";
42881
+ // @ts-expect-error - TS2769 - No overload matches this call.
42882
+ const $draggable = $(ReactDOM__default.findDOMNode(draggable));
42883
+ // eslint-disable-next-line react/no-string-refs
42884
+ // @ts-expect-error - TS2769 - No overload matches this call.
42885
+ const $bank = $(ReactDOM__default.findDOMNode(this.refs.bank));
42886
+ // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
42887
+ const draggableOffset = $draggable.offset();
42888
+ // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
42889
+ const bankOffset = $bank.offset();
42890
+ // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
42891
+ const draggableHeight = $draggable.outerHeight(true);
42892
+ // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
42893
+ const bankHeight = $bank.outerHeight(true);
42894
+ // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
42895
+ const bankWidth = $bank.outerWidth(true);
42896
+ // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
42897
+ const draggableWidth = $draggable.outerWidth(true);
42872
42898
  if (isHorizontal) {
42873
42899
  return draggableOffset.top + draggableHeight / 2 < bankOffset.top + bankHeight;
42874
42900
  }
@@ -43017,8 +43043,8 @@ Orderer.defaultProps = {
43017
43043
  current: [],
43018
43044
  options: [],
43019
43045
  correctOptions: [],
43020
- height: NORMAL,
43021
- layout: HORIZONTAL,
43046
+ height: "normal",
43047
+ layout: "horizontal",
43022
43048
  linterContext: linterContextDefault
43023
43049
  };
43024
43050
  var Orderer$1 = {
@@ -43297,18 +43323,36 @@ const getPromptJSON$3 = () => {
43297
43323
  };
43298
43324
 
43299
43325
  const {
43300
- deepEq: deepEq$1
43326
+ deepEq: deepEq$2
43301
43327
  } = Util;
43302
- function scorePlotter(userInput, rubric) {
43303
- if (deepEq$1(userInput, rubric.starting)) {
43328
+
43329
+ /**
43330
+ * Checks user input to confirm it is not the same as the starting values for the graph.
43331
+ * This means the user has modified the graph, and the question can be scored.
43332
+ *
43333
+ * @see 'scorePlotter' for more details on scoring.
43334
+ */
43335
+ function validatePlotter(userInput, validationData) {
43336
+ if (deepEq$2(userInput, validationData.starting)) {
43304
43337
  return {
43305
43338
  type: "invalid",
43306
43339
  message: null
43307
43340
  };
43308
43341
  }
43342
+ return null;
43343
+ }
43344
+
43345
+ const {
43346
+ deepEq: deepEq$1
43347
+ } = Util;
43348
+ function scorePlotter(userInput, scoringData) {
43349
+ const validationError = validatePlotter(userInput, scoringData);
43350
+ if (validationError) {
43351
+ return validationError;
43352
+ }
43309
43353
  return {
43310
43354
  type: "points",
43311
- earned: deepEq$1(userInput, rubric.correct) ? 1 : 0,
43355
+ earned: deepEq$1(userInput, scoringData.correct) ? 1 : 0,
43312
43356
  total: 1,
43313
43357
  message: null
43314
43358
  };
@@ -44193,7 +44237,14 @@ const getPromptJSON$1 = userInput => {
44193
44237
  };
44194
44238
  };
44195
44239
 
44196
- function scoreSorter(userInput, rubric) {
44240
+ /**
44241
+ * Checks user input for the sorter widget to ensure that the user has made
44242
+ * changes before attempting to score the widget.
44243
+ * @param userInput
44244
+ * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
44245
+ * for more details on how the sorter widget is scored.
44246
+ */
44247
+ function validateSorter(userInput) {
44197
44248
  // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44198
44249
  // prevents the "Check" button from becoming active. We want the user
44199
44250
  // to make a change before trying to move forward. This makes an
@@ -44206,6 +44257,14 @@ function scoreSorter(userInput, rubric) {
44206
44257
  message: null
44207
44258
  };
44208
44259
  }
44260
+ return null;
44261
+ }
44262
+
44263
+ function scoreSorter(userInput, rubric) {
44264
+ const validationError = validateSorter(userInput);
44265
+ if (validationError) {
44266
+ return validationError;
44267
+ }
44209
44268
  const correct = Util.deepEq(userInput.options, rubric.correct);
44210
44269
  return {
44211
44270
  type: "points",
@@ -44784,7 +44843,7 @@ const init = function init() {
44784
44843
 
44785
44844
  // This file is processed by a Rollup plugin (replace) to inject the production
44786
44845
  const libName = "@khanacademy/perseus";
44787
- const libVersion = "43.0.0";
44846
+ const libVersion = "43.1.0";
44788
44847
  addLibraryVersionToPerseusDebug(libName, libVersion);
44789
44848
 
44790
44849
  const apiVersion = {
@@ -46388,7 +46447,7 @@ const str = {
46388
46447
  }
46389
46448
  };
46390
46449
 
46391
- const _excluded = ["item", "children", "shape", "serializedState", "onSerializedStateUpdated"];
46450
+ const _excluded$1 = ["item", "children", "shape", "serializedState", "onSerializedStateUpdated"];
46392
46451
 
46393
46452
  // TODO(mdr)
46394
46453
  // TODO(mdr)
@@ -46473,7 +46532,7 @@ class MultiRenderer extends React.Component {
46473
46532
  // explicitly pulling them out of `this.props` so I don't pass them to
46474
46533
  // `<Renderer>`. I'm not sure how else to do this.
46475
46534
  const _this$props = this.props,
46476
- otherProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
46535
+ otherProps = _objectWithoutPropertiesLoose(_this$props, _excluded$1);
46477
46536
  return _extends({}, otherProps, {
46478
46537
  strings: this.context.strings
46479
46538
  });
@@ -48277,6 +48336,27 @@ function pair(parseA, parseB) {
48277
48336
  };
48278
48337
  }
48279
48338
 
48339
+ function pipeParsers(p) {
48340
+ return new ParserPipeline(p);
48341
+ }
48342
+ class ParserPipeline {
48343
+ constructor(parser) {
48344
+ this.parser = parser;
48345
+ }
48346
+ then(nextParser) {
48347
+ return new ParserPipeline(composeParsers(this.parser, nextParser));
48348
+ }
48349
+ }
48350
+ function composeParsers(parserA, parserB) {
48351
+ return (rawValue, ctx) => {
48352
+ const partialResult = parserA(rawValue, ctx);
48353
+ if (isFailure(partialResult)) {
48354
+ return partialResult;
48355
+ }
48356
+ return parserB(partialResult.value, ctx);
48357
+ };
48358
+ }
48359
+
48280
48360
  function record(parseKey, parseValue) {
48281
48361
  return (rawValue, ctx) => {
48282
48362
  if (!isObject(rawValue)) {
@@ -48357,6 +48437,20 @@ function either(parseA, parseB) {
48357
48437
  };
48358
48438
  }
48359
48439
 
48440
+ function defaulted(parser, fallback) {
48441
+ return (rawValue, ctx) => {
48442
+ if (rawValue == null) {
48443
+ return success(fallback(rawValue));
48444
+ }
48445
+ return parser(rawValue, ctx);
48446
+ };
48447
+ }
48448
+
48449
+ const parseImages = defaulted(record(string, object({
48450
+ width: number,
48451
+ height: number
48452
+ })), () => ({}));
48453
+
48360
48454
  function parseWidget(parseType, parseOptions) {
48361
48455
  return object({
48362
48456
  type: parseType,
@@ -48365,10 +48459,21 @@ function parseWidget(parseType, parseOptions) {
48365
48459
  alignment: optional(string),
48366
48460
  options: parseOptions,
48367
48461
  key: optional(number),
48368
- version: object({
48462
+ version: optional(object({
48369
48463
  major: number,
48370
48464
  minor: number
48371
- })
48465
+ }))
48466
+ });
48467
+ }
48468
+ function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
48469
+ return object({
48470
+ type: parseType,
48471
+ static: optional(boolean),
48472
+ graded: optional(boolean),
48473
+ alignment: optional(string),
48474
+ options: parseOptions,
48475
+ key: optional(number),
48476
+ version: parseVersion
48372
48477
  });
48373
48478
  }
48374
48479
 
@@ -48376,7 +48481,7 @@ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48376
48481
  items: array(string),
48377
48482
  categories: array(string),
48378
48483
  randomizeItems: boolean,
48379
- static: boolean,
48484
+ static: defaulted(boolean, () => false),
48380
48485
  values: array(number),
48381
48486
  highlightLint: optional(boolean),
48382
48487
  linterContext: optional(object({
@@ -48408,7 +48513,7 @@ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48408
48513
 
48409
48514
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48410
48515
  placeholder: string,
48411
- static: boolean,
48516
+ static: defaulted(boolean, () => false),
48412
48517
  choices: array(object({
48413
48518
  content: string,
48414
48519
  correct: boolean
@@ -48431,9 +48536,12 @@ const parseAnswerForm = object({
48431
48536
  form: boolean,
48432
48537
  simplify: boolean,
48433
48538
  considered: enumeration("correct", "wrong", "ungraded"),
48434
- key: optional(string)
48539
+ key: pipeParsers(optional(union(string).or(number).parser)).then((key, ctx) => ctx.success(String(key))).parser
48435
48540
  });
48436
- const parseExpressionWidget = parseWidget(constant("expression"), object({
48541
+ const parseExpressionWidgetV1 = parseWidgetWithVersion(object({
48542
+ major: constant(1),
48543
+ minor: number
48544
+ }), constant("expression"), object({
48437
48545
  answerForms: array(parseAnswerForm),
48438
48546
  functions: array(string),
48439
48547
  times: boolean,
@@ -48442,23 +48550,68 @@ const parseExpressionWidget = parseWidget(constant("expression"), object({
48442
48550
  buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48443
48551
  buttonsVisible: optional(enumeration("always", "never", "focused"))
48444
48552
  }));
48553
+ const parseExpressionWidgetV0 = parseWidgetWithVersion(optional(object({
48554
+ major: constant(0),
48555
+ minor: number
48556
+ })), constant("expression"), object({
48557
+ functions: array(string),
48558
+ times: boolean,
48559
+ visibleLabel: optional(string),
48560
+ ariaLabel: optional(string),
48561
+ form: boolean,
48562
+ simplify: boolean,
48563
+ value: string,
48564
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48565
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48566
+ }));
48567
+ function migrateV0ToV1(widget, ctx) {
48568
+ const {
48569
+ options
48570
+ } = widget;
48571
+ return ctx.success(_extends({}, widget, {
48572
+ version: {
48573
+ major: 1,
48574
+ minor: 0
48575
+ },
48576
+ options: {
48577
+ times: options.times,
48578
+ buttonSets: options.buttonSets,
48579
+ functions: options.functions,
48580
+ buttonsVisible: options.buttonsVisible,
48581
+ visibleLabel: options.visibleLabel,
48582
+ ariaLabel: options.ariaLabel,
48583
+ answerForms: [{
48584
+ considered: "correct",
48585
+ form: options.form,
48586
+ simplify: options.simplify,
48587
+ value: options.value
48588
+ }]
48589
+ }
48590
+ }));
48591
+ }
48592
+ const parseExpressionWidget = union(parseExpressionWidgetV1).or(pipeParsers(parseExpressionWidgetV0).then(migrateV0ToV1).parser).parser;
48445
48593
 
48446
- const parsePerseusRenderer = object({
48447
- content: string,
48594
+ const parsePerseusRenderer = defaulted(object({
48595
+ // TODO(benchristel): content is also defaulted to empty string in
48596
+ // renderer.tsx. See if we can remove one default or the other.
48597
+ content: defaulted(string, () => ""),
48448
48598
  // This module has an import cycle with parseWidgetsMap, because the
48449
48599
  // `group` widget can contain another renderer.
48450
48600
  // The anonymous function below ensures that we don't try to access
48451
48601
  // parseWidgetsMap before it's defined.
48452
- widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48602
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
48453
48603
  metadata: optional(array(string)),
48454
- images: record(string, object({
48455
- width: number,
48456
- height: number
48457
- }))
48458
- });
48604
+ images: parseImages
48605
+ }),
48606
+ // Default value
48607
+ () => ({
48608
+ content: "",
48609
+ widgets: {},
48610
+ images: {}
48611
+ }));
48459
48612
 
48460
48613
  const parseGradedGroupWidgetOptions = object({
48461
- title: string,
48614
+ title: defaulted(string, () => ""),
48462
48615
  hasHint: optional(nullable(boolean)),
48463
48616
  // This module has an import cycle with parsePerseusRenderer.
48464
48617
  // The anonymous function below ensures that we don't try to access
@@ -48552,7 +48705,7 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
48552
48705
  height: union(number).or(string).parser,
48553
48706
  allowFullScreen: boolean,
48554
48707
  allowTopNavigation: optional(boolean),
48555
- static: boolean
48708
+ static: defaulted(boolean, () => false)
48556
48709
  }));
48557
48710
 
48558
48711
  const parsePerseusImageBackground = object({
@@ -48931,13 +49084,13 @@ const parseMatcherWidget = parseWidget(constant("matcher"), object({
48931
49084
  padding: boolean
48932
49085
  }));
48933
49086
 
48934
- const parseMatrixWidget = parseWidget(constant("matrix"), object({
48935
- prefix: string,
48936
- suffix: string,
49087
+ const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
49088
+ prefix: optional(string),
49089
+ suffix: optional(string),
48937
49090
  answers: array(array(number)),
48938
- cursorPosition: array(number),
49091
+ cursorPosition: optional(array(number)),
48939
49092
  matrixBoardSize: array(number),
48940
- static: boolean
49093
+ static: optional(boolean)
48941
49094
  }));
48942
49095
 
48943
49096
  const parseMeasurerWidget = parseWidget(constant("measurer"), object({
@@ -49007,8 +49160,8 @@ const parseOrdererWidget = parseWidget(constant("orderer"), object({
49007
49160
  options: array(parseRenderer),
49008
49161
  correctOptions: array(parseRenderer),
49009
49162
  otherOptions: array(parseRenderer),
49010
- height: string,
49011
- layout: string
49163
+ height: enumeration("normal", "auto"),
49164
+ layout: enumeration("horizontal", "vertical")
49012
49165
  }));
49013
49166
 
49014
49167
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49053,7 +49206,7 @@ const parsePythonProgramWidget = parseWidget(constant("python-program"), object(
49053
49206
 
49054
49207
  const parseRadioWidget = parseWidget(constant("radio"), object({
49055
49208
  choices: array(object({
49056
- content: string,
49209
+ content: defaulted(string, () => ""),
49057
49210
  clue: optional(string),
49058
49211
  correct: optional(boolean),
49059
49212
  isNoneOfTheAbove: optional(boolean),
@@ -49214,26 +49367,42 @@ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
49214
49367
  const parseHint = object({
49215
49368
  replace: optional(boolean),
49216
49369
  content: string,
49217
- widgets: parseWidgetsMap,
49370
+ widgets: defaulted(parseWidgetsMap, () => ({})),
49218
49371
  metadata: optional(array(string)),
49219
- images: record(string, object({
49220
- width: number,
49221
- height: number
49222
- }))
49372
+ images: parseImages
49223
49373
  });
49224
49374
 
49375
+ const _excluded = ["type", "options"];
49225
49376
  object({
49226
49377
  question: parsePerseusRenderer,
49227
- hints: array(parseHint),
49228
- answerArea: record(enumeration(...ItemExtras), boolean),
49229
- itemDataVersion: object({
49378
+ hints: defaulted(array(parseHint), () => []),
49379
+ answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...ItemExtras), boolean)).parser,
49380
+ itemDataVersion: optional(object({
49230
49381
  major: number,
49231
49382
  minor: number
49232
- }),
49383
+ })),
49233
49384
  // Deprecated field
49234
49385
  answer: any
49235
49386
  });
49236
49387
 
49388
+ // Some answerAreas have extra fields, like:
49389
+ //
49390
+ // "answerArea": {
49391
+ // "type": "multiple",
49392
+ // "options": {
49393
+ // "content": "",
49394
+ // "images": {},
49395
+ // "widgets": {}
49396
+ // }
49397
+ // }
49398
+ //
49399
+ // The "type" and "options" fields don't seem to be used anywhere. This
49400
+ // migration function removes them.
49401
+ function migrateAnswerArea(rawValue, ctx) {
49402
+ const rest = _objectWithoutPropertiesLoose(rawValue, _excluded);
49403
+ return ctx.success(rest);
49404
+ }
49405
+
49237
49406
  /**
49238
49407
  * Helper to parse PerseusItem JSON
49239
49408
  * Why not just use JSON.parse? We want: