@khanacademy/perseus 34.1.0 → 35.1.1

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 (84) hide show
  1. package/dist/es/index.js +1109 -974
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/strings.js +8 -2
  4. package/dist/es/strings.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +1032 -905
  7. package/dist/index.js.map +1 -1
  8. package/dist/mixins/widget-prop-denylist.d.ts +0 -1
  9. package/dist/perseus-api.d.ts +5 -5
  10. package/dist/perseus-types.d.ts +13 -19
  11. package/dist/renderer.d.ts +4 -4
  12. package/dist/server-item-renderer.d.ts +5 -9
  13. package/dist/strings.d.ts +3 -0
  14. package/dist/strings.js +8 -2
  15. package/dist/strings.js.map +1 -1
  16. package/dist/types.d.ts +3 -6
  17. package/dist/validation.types.d.ts +114 -0
  18. package/dist/widgets/categorizer/categorizer-validator.d.ts +2 -2
  19. package/dist/widgets/categorizer/categorizer.d.ts +6 -6
  20. package/dist/widgets/cs-program/cs-program-validator.d.ts +3 -0
  21. package/dist/widgets/cs-program/cs-program.d.ts +5 -10
  22. package/dist/widgets/definition/definition.d.ts +3 -4
  23. package/dist/widgets/dropdown/dropdown-validator.d.ts +2 -2
  24. package/dist/widgets/dropdown/dropdown.d.ts +5 -5
  25. package/dist/widgets/explanation/explanation.d.ts +3 -2
  26. package/dist/widgets/expression/expression-validator.d.ts +2 -2
  27. package/dist/widgets/expression/expression.d.ts +46 -28
  28. package/dist/widgets/expression/expression.types.d.ts +0 -1
  29. package/dist/widgets/graded-group/graded-group.d.ts +2 -2
  30. package/dist/widgets/graded-group-set/graded-group-set.d.ts +2 -2
  31. package/dist/widgets/grapher/grapher-validator.d.ts +4 -0
  32. package/dist/widgets/grapher/grapher.d.ts +6 -7
  33. package/dist/widgets/grapher/util.d.ts +0 -2
  34. package/dist/widgets/group/group.d.ts +3 -2
  35. package/dist/widgets/iframe/iframe-validator.d.ts +3 -0
  36. package/dist/widgets/iframe/iframe.d.ts +7 -11
  37. package/dist/widgets/image/image.d.ts +3 -4
  38. package/dist/widgets/input-number/input-number-validator.d.ts +3 -3
  39. package/dist/widgets/input-number/input-number.d.ts +7 -10
  40. package/dist/widgets/interaction/interaction.d.ts +3 -3
  41. package/dist/widgets/interactive-graph.d.ts +11 -14
  42. package/dist/widgets/interactive-graphs/backgrounds/axis-ticks.d.ts +1 -1
  43. package/dist/widgets/interactive-graphs/graph-locked-layer.d.ts +2 -0
  44. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +15 -2
  45. package/dist/widgets/interactive-graphs/interactive-graph-validator.d.ts +4 -0
  46. package/dist/widgets/interactive-graphs/locked-figures/locked-ellipse.d.ts +5 -1
  47. package/dist/widgets/interactive-graphs/locked-figures/locked-function.d.ts +5 -1
  48. package/dist/widgets/interactive-graphs/locked-figures/locked-line.d.ts +2 -0
  49. package/dist/widgets/interactive-graphs/locked-figures/locked-point.d.ts +5 -1
  50. package/dist/widgets/interactive-graphs/locked-figures/locked-polygon.d.ts +5 -1
  51. package/dist/widgets/interactive-graphs/locked-figures/locked-vector.d.ts +5 -1
  52. package/dist/widgets/interactive-graphs/mafs-state-to-interactive-graph.d.ts +3 -0
  53. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +5 -2
  54. package/dist/widgets/label-image/label-image-validator.d.ts +10 -0
  55. package/dist/widgets/label-image/label-image.d.ts +15 -10
  56. package/dist/widgets/label-image/types.d.ts +0 -4
  57. package/dist/widgets/matcher/matcher-validator.d.ts +4 -0
  58. package/dist/widgets/matcher/matcher.d.ts +6 -5
  59. package/dist/widgets/matrix/matrix-validator.d.ts +5 -0
  60. package/dist/widgets/matrix/matrix.d.ts +6 -5
  61. package/dist/widgets/molecule/molecule.d.ts +2 -1
  62. package/dist/widgets/number-line/number-line-validator.d.ts +4 -0
  63. package/dist/widgets/number-line/number-line.d.ts +7 -4
  64. package/dist/widgets/numeric-input/numeric-input-validator.d.ts +2 -2
  65. package/dist/widgets/numeric-input/numeric-input.d.ts +7 -7
  66. package/dist/widgets/orderer/orderer.d.ts +4 -4
  67. package/dist/widgets/passage/passage.d.ts +3 -4
  68. package/dist/widgets/passage-ref/passage-ref.d.ts +3 -11
  69. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +2 -1
  70. package/dist/widgets/phet-simulation/phet-simulation.d.ts +2 -2
  71. package/dist/widgets/plotter/plotter-validator.d.ts +4 -0
  72. package/dist/widgets/plotter/plotter.d.ts +6 -5
  73. package/dist/widgets/radio/radio-component.d.ts +6 -13
  74. package/dist/widgets/radio/radio-validator.d.ts +5 -0
  75. package/dist/widgets/sorter/sorter-validator.d.ts +4 -0
  76. package/dist/widgets/sorter/sorter.d.ts +7 -9
  77. package/dist/widgets/table/table-validator.d.ts +5 -0
  78. package/dist/widgets/table/table.d.ts +6 -5
  79. package/dist/widgets/video/video.d.ts +2 -2
  80. package/package.json +1 -1
  81. package/dist/widgets/categorizer/categorizer.types.d.ts +0 -5
  82. package/dist/widgets/dropdown/dropdown.types.d.ts +0 -5
  83. package/dist/widgets/input-number/input-number.types.d.ts +0 -2
  84. package/dist/widgets/numeric-input/numeric-input.types.d.ts +0 -5
package/dist/es/index.js CHANGED
@@ -246,7 +246,10 @@ const mockStrings = {
246
246
  cos: "cos",
247
247
  tan: "tan",
248
248
  simulationLoadFail: "Sorry, this simulation cannot load.",
249
- simulationLocaleWarning: "Sorry, this simulation isn't available in your language."
249
+ simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
250
+ addPoint: "Add Point",
251
+ removePoint: "Remove Point",
252
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph"
250
253
  };
251
254
 
252
255
  /**
@@ -749,7 +752,7 @@ const denylist = [
749
752
  // (technically the renderer still adds them)
750
753
  "key", "ref",
751
754
  // added by src/renderer.jsx
752
- "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "questionCompleted", "findWidgets",
755
+ "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "widgetIsOpen", "questionCompleted", "findWidgets",
753
756
  // added by src/editor.jsx, for widgets removing themselves
754
757
  // this is soooo not the right place for this, but alas.
755
758
  "onRemove",
@@ -757,15 +760,6 @@ const denylist = [
757
760
  "id",
758
761
  // Callbacks and items for interaction handling
759
762
  "onBlur", "onFocus", "trackInteraction", "keypadElement"];
760
- const removeDenylistProps = props => {
761
- const newProps = _extends({}, props);
762
- for (const prop of denylist) {
763
- if (prop in newProps) {
764
- delete newProps[prop];
765
- }
766
- }
767
- return newProps;
768
- };
769
763
 
770
764
  /* eslint-disable @babel/no-invalid-this */
771
765
  const USAGE = "Usage:\n" + " this.change({propName: 5}, callback);\n" + ' this.change("propName", 5, callback);\n' + ' this.change("propName")';
@@ -1030,8 +1024,6 @@ StubTagEditor.defaultProps = {
1030
1024
  * * nothing if it is purely a bug fix.
1031
1025
  *
1032
1026
  * Callbacks passed to Renderer/ItemRenderer:
1033
- * * onInputError:
1034
- * Called when there is an error grading a widget
1035
1027
  * * onFocusChange: (newFocusPath, oldFocusPath, keypadDOMNode)
1036
1028
  * Called when the user focus changes. The first two parameters are `path`
1037
1029
  * arrays uniquely identifying the respect inputs. The third parameter,
@@ -1054,7 +1046,6 @@ StubTagEditor.defaultProps = {
1054
1046
  const ApiOptions = {
1055
1047
  propTypes: PropTypes.shape({
1056
1048
  isArticle: PropTypes.bool.isRequired,
1057
- onInputError: PropTypes.func.isRequired,
1058
1049
  onFocusChange: PropTypes.func.isRequired,
1059
1050
  GroupMetadataEditor: PropTypes.func.isRequired,
1060
1051
  showAlignmentOptions: PropTypes.bool.isRequired,
@@ -1138,7 +1129,6 @@ const ApiOptions = {
1138
1129
  defaults: {
1139
1130
  isArticle: false,
1140
1131
  isMobile: false,
1141
- onInputError: function () {},
1142
1132
  onFocusChange: function () {},
1143
1133
  GroupMetadataEditor: StubTagEditor,
1144
1134
  showAlignmentOptions: false,
@@ -2150,9 +2140,7 @@ const getDecimalSeparator = locale => {
2150
2140
  * show the user an error. TODO(joel) - what error?
2151
2141
  * - Otherwise, pass through the resulting points and message.
2152
2142
  */
2153
- function expressionValidator(userInput, rubric,
2154
- // @ts-expect-error - TS2322 - Type '() => void' is not assignable to type 'OnInputErrorFunctionType'.
2155
- onInputError = function () {}, strings, locale) {
2143
+ function expressionValidator(userInput, rubric, strings, locale) {
2156
2144
  const options = _.clone(rubric);
2157
2145
  _.extend(options, {
2158
2146
  decimal_separator: getDecimalSeparator(locale)
@@ -2252,14 +2240,9 @@ onInputError = function () {}, strings, locale) {
2252
2240
  };
2253
2241
  }
2254
2242
  if (matchingAnswerForm.considered === "ungraded") {
2255
- // We matched an ungraded answer form - return "invalid", which
2256
- // will let the user try again with no penalty
2257
- const apiResult = onInputError(null,
2258
- // Reserved for some widget identifier
2259
- userInput, matchMessage);
2260
2243
  return {
2261
2244
  type: "invalid",
2262
- message: apiResult === false ? null : matchMessage
2245
+ message: matchMessage
2263
2246
  };
2264
2247
  }
2265
2248
  // We matched a graded answer form, so we can now tell the user
@@ -2348,14 +2331,9 @@ class Expression extends React.Component {
2348
2331
  showErrorStyle: false
2349
2332
  });
2350
2333
  if (!this.parse(this.props.value, this.props).parsed) {
2351
- const apiResult = this.props.apiOptions.onInputError(null,
2352
- // reserved for some widget identifier
2353
- this.props.value, this.context.strings.ERROR_TITLE);
2354
- if (apiResult !== false) {
2355
- this.setState({
2356
- invalid: true
2357
- });
2358
- }
2334
+ this.setState({
2335
+ invalid: true
2336
+ });
2359
2337
  }
2360
2338
  }
2361
2339
  };
@@ -2369,12 +2347,12 @@ class Expression extends React.Component {
2369
2347
  });
2370
2348
  }
2371
2349
  };
2372
- this.simpleValidate = (_ref, onInputError) => {
2350
+ this.simpleValidate = _ref => {
2373
2351
  let {
2374
2352
  scoring
2375
2353
  } = _ref,
2376
2354
  rubric = _objectWithoutPropertiesLoose(_ref, _excluded$5);
2377
- const score = expressionValidator(this.getUserInput(), rubric, onInputError || function () {}, this.context.strings, this.context.locale);
2355
+ const score = expressionValidator(this.getUserInput(), rubric, this.context.strings, this.context.locale);
2378
2356
 
2379
2357
  // "scoring" is a flag that indicates when we are checking answers.
2380
2358
  // otherwise, we may just be checking validity after changes.
@@ -2390,9 +2368,6 @@ class Expression extends React.Component {
2390
2368
  }
2391
2369
  return score;
2392
2370
  };
2393
- this.getUserInput = () => {
2394
- return Expression.getUserInputFromProps(this.props);
2395
- };
2396
2371
  this.change = (...args) => {
2397
2372
  return change.apply(this, args);
2398
2373
  };
@@ -2465,10 +2440,8 @@ class Expression extends React.Component {
2465
2440
  };
2466
2441
  }
2467
2442
  // TODO remove this in favor of just using expressionValidator
2468
- static validate(userInput, rubric,
2469
- // @ts-expect-error - TS2322 - Type '() => void' is not assignable to type 'OnInputErrorFunctionType'.
2470
- onInputError = function () {}, strings, locale) {
2471
- return expressionValidator(userInput, rubric, onInputError, strings, locale);
2443
+ static validate(userInput, rubric, strings, locale) {
2444
+ return expressionValidator(userInput, rubric, strings, locale);
2472
2445
  }
2473
2446
  static getUserInputFromProps(props) {
2474
2447
  return normalizeTex(props.value);
@@ -2482,6 +2455,9 @@ class Expression extends React.Component {
2482
2455
  }
2483
2456
  //#endregion
2484
2457
 
2458
+ getUserInput() {
2459
+ return Expression.getUserInputFromProps(this.props);
2460
+ }
2485
2461
  // HACK(joel)
2486
2462
  insert(keyPressed) {
2487
2463
  // eslint-disable-next-line react/no-string-refs
@@ -3170,7 +3146,7 @@ function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
3170
3146
  /**
3171
3147
  * Approximate equality on numbers and primitives.
3172
3148
  */
3173
- function eq$3(x, y) {
3149
+ function eq$4(x, y) {
3174
3150
  if (typeof x === "number" && typeof y === "number") {
3175
3151
  return Math.abs(x - y) < 1e-9;
3176
3152
  }
@@ -3197,7 +3173,7 @@ function deepEq$5(x, y) {
3197
3173
  return false;
3198
3174
  }
3199
3175
  if (typeof x === "function" && typeof y === "function") {
3200
- return eq$3(x, y);
3176
+ return eq$4(x, y);
3201
3177
  }
3202
3178
  if (typeof x === "function" || typeof y === "function") {
3203
3179
  return false;
@@ -3214,7 +3190,7 @@ function deepEq$5(x, y) {
3214
3190
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
3215
3191
  return false;
3216
3192
  }
3217
- return eq$3(x, y);
3193
+ return eq$4(x, y);
3218
3194
  }
3219
3195
 
3220
3196
  /**
@@ -3514,7 +3490,7 @@ const Util = {
3514
3490
  gridStepFromTickStep,
3515
3491
  tickStepFromNumTicks,
3516
3492
  constrainedTickStepsFromTickSteps,
3517
- eq: eq$3,
3493
+ eq: eq$4,
3518
3494
  deepEq: deepEq$5,
3519
3495
  parseQueryString,
3520
3496
  updateQueryString: updateQueryString$2,
@@ -10837,7 +10813,7 @@ class WrappedLine extends WrappedDrawing {
10837
10813
  * A collection of geomtry-related utility functions
10838
10814
  */
10839
10815
  const {
10840
- eq: eq$2,
10816
+ eq: eq$3,
10841
10817
  deepEq: deepEq$4
10842
10818
  } = Util;
10843
10819
 
@@ -10847,7 +10823,7 @@ const {
10847
10823
 
10848
10824
  // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10849
10825
  function sign(val) {
10850
- if (eq$2(val, 0)) {
10826
+ if (eq$3(val, 0)) {
10851
10827
  return 0;
10852
10828
  }
10853
10829
  return val > 0 ? 1 : -1;
@@ -10859,7 +10835,7 @@ function ccw(a, b, c) {
10859
10835
  return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10860
10836
  }
10861
10837
  function collinear(a, b, c) {
10862
- return eq$2(ccw(a, b, c), 0);
10838
+ return eq$3(ccw(a, b, c), 0);
10863
10839
  }
10864
10840
 
10865
10841
  // Given rect bounding points A and B, whether point C is inside the rect
@@ -11004,7 +10980,7 @@ function similar(coords1, coords2, tolerance) {
11004
10980
  return pair[0] / pair[1];
11005
10981
  });
11006
10982
  const same = _.all(factors, function (factor) {
11007
- return eq$2(factors[0], factor);
10983
+ return eq$3(factors[0], factor);
11008
10984
  });
11009
10985
  const congruentEnough = _.all(sidePairs, function (pair) {
11010
10986
  return number.equal(pair[0], pair[1], tolerance);
@@ -11056,7 +11032,7 @@ function rotate(array, n) {
11056
11032
  return array.slice(n).concat(array.slice(0, n));
11057
11033
  }
11058
11034
  function getLineEquation(first, second) {
11059
- if (eq$2(first[0], second[0])) {
11035
+ if (eq$3(first[0], second[0])) {
11060
11036
  return "x = " + first[0].toFixed(3);
11061
11037
  }
11062
11038
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -14677,7 +14653,7 @@ const pluck = function pluck(table, subKey) {
14677
14653
  * });
14678
14654
  * {a: 2, b: 3}
14679
14655
  */
14680
- const mapObject$2 = function mapObject(obj, lambda) {
14656
+ const mapObject$1 = function mapObject(obj, lambda) {
14681
14657
  const result = {};
14682
14658
  _.each(_.keys(obj), function (key) {
14683
14659
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14703,7 +14679,7 @@ const mapObjectFromArray$1 = function mapObjectFromArray(arr, lambda) {
14703
14679
  };
14704
14680
  var objective_ = {
14705
14681
  pluck: pluck,
14706
- mapObject: mapObject$2,
14682
+ mapObject: mapObject$1,
14707
14683
  mapObjectFromArray: mapObjectFromArray$1
14708
14684
  };
14709
14685
 
@@ -19596,7 +19572,7 @@ WidgetContainer.defaultProps = {
19596
19572
  };
19597
19573
 
19598
19574
  const {
19599
- mapObject: mapObject$1,
19575
+ mapObject,
19600
19576
  mapObjectFromArray
19601
19577
  } = objective_;
19602
19578
  const rContainsNonWhitespace = /\S/;
@@ -19624,9 +19600,6 @@ const isIdPathPrefix = function isIdPathPrefix(prefixArray, wholeArray) {
19624
19600
  });
19625
19601
  };
19626
19602
 
19627
- // The return type for getUserInput. Widgets have full control of what is
19628
- // returned so it's not easily typed (some widgets return a scalar (string),
19629
- // some return a custom-built object
19630
19603
  /**
19631
19604
  * The Widget type represents the common API that the Renderer uses to interact
19632
19605
  * with all widgets. All widgets must implement the methods in this API, unless
@@ -19636,6 +19609,7 @@ const isIdPathPrefix = function isIdPathPrefix(prefixArray, wholeArray) {
19636
19609
  * communicate with the individual widgets to coordinate actions such as
19637
19610
  * scoring, state serialization/deserialization, and focus management.
19638
19611
  */
19612
+
19639
19613
  class Renderer extends React.Component {
19640
19614
  constructor(_props) {
19641
19615
  super(_props);
@@ -19670,7 +19644,7 @@ class Renderer extends React.Component {
19670
19644
  };
19671
19645
  // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19672
19646
  this._getAllWidgetsInfo = props => {
19673
- return mapObject$1(props.widgets, (widgetInfo, widgetId) => {
19647
+ return mapObject(props.widgets, (widgetInfo, widgetId) => {
19674
19648
  if (!widgetInfo.type || !widgetInfo.alignment) {
19675
19649
  const newValues = {};
19676
19650
  if (!widgetInfo.type) {
@@ -19811,7 +19785,7 @@ class Renderer extends React.Component {
19811
19785
  * state from that instead of the current widget props.
19812
19786
  */
19813
19787
  this.getSerializedState = widgetProps => {
19814
- return mapObject$1(widgetProps || this.state.widgetProps, (props, widgetId) => {
19788
+ return mapObject(widgetProps || this.state.widgetProps, (props, widgetId) => {
19815
19789
  const widget = this.getWidgetInstance(widgetId);
19816
19790
  if (widget && widget.getSerializedState) {
19817
19791
  return widget.getSerializedState();
@@ -19847,7 +19821,7 @@ class Renderer extends React.Component {
19847
19821
  }
19848
19822
  };
19849
19823
  this.setState({
19850
- widgetProps: mapObject$1(serializedState, (props, widgetId) => {
19824
+ widgetProps: mapObject(serializedState, (props, widgetId) => {
19851
19825
  const widget = this.getWidgetInstance(widgetId);
19852
19826
  if (widget && widget.restoreSerializedState) {
19853
19827
  // Note that we probably can't call
@@ -20681,7 +20655,6 @@ class Renderer extends React.Component {
20681
20655
  */
20682
20656
  this.scoreWidgets = () => {
20683
20657
  const widgetProps = this.state.widgetInfo;
20684
- const onInputError = this.getApiOptions().onInputError;
20685
20658
  const gradedWidgetIds = _.filter(this.widgetIds, id => {
20686
20659
  const props = widgetProps[id];
20687
20660
  const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
@@ -20697,7 +20670,7 @@ class Renderer extends React.Component {
20697
20670
  if (widget && widget.simpleValidate) {
20698
20671
  widgetScores[id] = widget.simpleValidate(_extends({}, props == null ? void 0 : props.options, {
20699
20672
  scoring: true
20700
- }), onInputError);
20673
+ }));
20701
20674
  }
20702
20675
  });
20703
20676
  return widgetScores;
@@ -21738,7 +21711,7 @@ const answerTypes = {
21738
21711
  forms: "pi"
21739
21712
  }
21740
21713
  };
21741
- function inputNumberValidator(state, rubric, strings, onInputError = () => {}) {
21714
+ function inputNumberValidator(state, rubric, strings) {
21742
21715
  if (rubric.answerType == null) {
21743
21716
  rubric.answerType = "number";
21744
21717
  }
@@ -21762,13 +21735,9 @@ function inputNumberValidator(state, rubric, strings, onInputError = () => {}) {
21762
21735
  // TODO(eater): Seems silly to translate result to this invalid/points
21763
21736
  // thing and immediately translate it back in ItemRenderer.scoreInput()
21764
21737
  if (result.empty) {
21765
- // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
21766
- const apiResult = onInputError == null ? void 0 : onInputError(null,
21767
- // reserved for some widget identifier
21768
- state.currentValue, result.message);
21769
21738
  return {
21770
21739
  type: "invalid",
21771
- message: apiResult === false ? null : result.message
21740
+ message: result.message
21772
21741
  };
21773
21742
  }
21774
21743
  return {
@@ -21864,13 +21833,6 @@ class InputNumber extends React.Component {
21864
21833
  currentValue: newValue
21865
21834
  }, cb);
21866
21835
  };
21867
- this.getUserInput = () => {
21868
- return InputNumber.getUserInputFromProps(this.props);
21869
- };
21870
- this.simpleValidate = (rubric, onInputError) => {
21871
- onInputError = onInputError || function () {};
21872
- return inputNumberValidator(this.getUserInput(), rubric, this.context.strings, onInputError);
21873
- };
21874
21836
  this.examples = () => {
21875
21837
  const {
21876
21838
  strings
@@ -21881,8 +21843,8 @@ class InputNumber extends React.Component {
21881
21843
  return [strings.yourAnswer].concat(examples);
21882
21844
  };
21883
21845
  }
21884
- static validate(state, rubric, strings, onInputError = () => {}) {
21885
- return inputNumberValidator(state, rubric, strings, onInputError);
21846
+ static validate(state, rubric, strings) {
21847
+ return inputNumberValidator(state, rubric, strings);
21886
21848
  }
21887
21849
  static getUserInputFromProps(props) {
21888
21850
  return {
@@ -21899,6 +21861,12 @@ class InputNumber extends React.Component {
21899
21861
  }
21900
21862
  return answerString;
21901
21863
  }
21864
+ getUserInput() {
21865
+ return InputNumber.getUserInputFromProps(this.props);
21866
+ }
21867
+ simpleValidate(rubric) {
21868
+ return inputNumberValidator(this.getUserInput(), rubric, this.context.strings);
21869
+ }
21902
21870
  render() {
21903
21871
  if (this.props.apiOptions.customKeypad) {
21904
21872
  // TODO(charlie): Support "Review Mode".
@@ -22202,9 +22170,6 @@ class NumericInput extends React.Component {
22202
22170
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22203
22171
  return !noFormsAccepted && !allFormsAccepted;
22204
22172
  };
22205
- this.simpleValidate = rubric => {
22206
- return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22207
- };
22208
22173
  this.focus = () => {
22209
22174
  var _this$inputRef;
22210
22175
  (_this$inputRef = this.inputRef) == null || _this$inputRef.focus();
@@ -22234,9 +22199,6 @@ class NumericInput extends React.Component {
22234
22199
  currentValue: newValue
22235
22200
  }, cb);
22236
22201
  };
22237
- this.getUserInput = () => {
22238
- return NumericInput.getUserInputFromProps(this.props);
22239
- };
22240
22202
  this.handleChange = (newValue, cb) => {
22241
22203
  this.props.onChange({
22242
22204
  currentValue: newValue
@@ -22286,6 +22248,12 @@ class NumericInput extends React.Component {
22286
22248
  static validate(userInput, rubric, strings) {
22287
22249
  return numericInputValidator(userInput, rubric, strings);
22288
22250
  }
22251
+ simpleValidate(rubric) {
22252
+ return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22253
+ }
22254
+ getUserInput() {
22255
+ return NumericInput.getUserInputFromProps(this.props);
22256
+ }
22289
22257
  render() {
22290
22258
  let labelText = this.props.labelText;
22291
22259
  if (labelText == null || labelText === "") {
@@ -22442,11 +22410,6 @@ class PassageRef extends React.Component {
22442
22410
  lineRange: null,
22443
22411
  content: null
22444
22412
  };
22445
- // TODO(tamarab): getUserInput needs to be updated to only return
22446
- // props input by the user. Currently returns all the widget's props.
22447
- this.getUserInput = () => {
22448
- return removeDenylistProps(this.props);
22449
- };
22450
22413
  this.change = (...args) => {
22451
22414
  return change.apply(this, args);
22452
22415
  };
@@ -22499,6 +22462,11 @@ class PassageRef extends React.Component {
22499
22462
  window.removeEventListener("resize", this._throttledUpdateRange);
22500
22463
  this._isMounted = false;
22501
22464
  }
22465
+
22466
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22467
+ getUserInput() {
22468
+ return null;
22469
+ }
22502
22470
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22503
22471
  simpleValidate() {
22504
22472
  return noopValidator();
@@ -23904,6 +23872,56 @@ const styles$t = StyleSheet.create({
23904
23872
  }
23905
23873
  });
23906
23874
 
23875
+ function radioValidator(userInput, rubric, strings) {
23876
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
23877
+ return sum + (selected ? 1 : 0);
23878
+ }, 0);
23879
+ if (numSelected === 0) {
23880
+ return {
23881
+ type: "invalid",
23882
+ message: null
23883
+ };
23884
+ }
23885
+
23886
+ // TODO: should numCorrect actually be on the rubric
23887
+ // instead of the userInput?
23888
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
23889
+ return {
23890
+ type: "invalid",
23891
+ message: strings.chooseCorrectNum
23892
+ };
23893
+ // If NOTA and some other answer are checked, ...
23894
+ }
23895
+
23896
+ // TODO: should noneOfTheAboveSelected be replaced with a
23897
+ // combination of choicesSelected and noneOfTheAboveIndex?
23898
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
23899
+ return {
23900
+ type: "invalid",
23901
+ message: strings.notNoneOfTheAbove
23902
+ };
23903
+ }
23904
+ const correct = userInput.choicesSelected.every((selected, i) => {
23905
+ let isCorrect;
23906
+ // TODO: should noneOfTheAboveIndex actually be on the rubric
23907
+ // instead of the userInput?
23908
+ if (userInput.noneOfTheAboveIndex === i) {
23909
+ isCorrect = rubric.choices.every((choice, j) => {
23910
+ return i === j || !choice.correct;
23911
+ });
23912
+ } else {
23913
+ isCorrect = !!rubric.choices[i].correct;
23914
+ }
23915
+ return isCorrect === selected;
23916
+ });
23917
+ return {
23918
+ type: "points",
23919
+ earned: correct ? 1 : 0,
23920
+ total: 1,
23921
+ message: null
23922
+ };
23923
+ }
23924
+
23907
23925
  // RenderProps is the return type for radio.jsx#transform
23908
23926
 
23909
23927
  class Radio$1 extends React.Component {
@@ -23998,12 +24016,6 @@ class Radio$1 extends React.Component {
23998
24016
  });
23999
24017
  this.props.trackInteraction();
24000
24018
  };
24001
- this.getUserInput = () => {
24002
- return Radio$1.getUserInputFromProps(this.props);
24003
- };
24004
- this.simpleValidate = rubric => {
24005
- return Radio$1.validate(this.getUserInput(), rubric, this.context.strings);
24006
- };
24007
24019
  /**
24008
24020
  * Turn on rationale display for the currently selected choices. Note that
24009
24021
  * this leaves rationales on for choices that are already showing
@@ -24064,46 +24076,7 @@ class Radio$1 extends React.Component {
24064
24076
  };
24065
24077
  }
24066
24078
  static validate(userInput, rubric, strings) {
24067
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
24068
- return sum + (selected ? 1 : 0);
24069
- }, 0);
24070
- if (numSelected === 0) {
24071
- return {
24072
- type: "invalid",
24073
- message: null
24074
- };
24075
- }
24076
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
24077
- return {
24078
- type: "invalid",
24079
- message: strings.chooseCorrectNum
24080
- };
24081
- // If NOTA and some other answer are checked, ...
24082
- }
24083
-
24084
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
24085
- return {
24086
- type: "invalid",
24087
- message: strings.notNoneOfTheAbove
24088
- };
24089
- }
24090
- const correct = userInput.choicesSelected.every((selected, i) => {
24091
- let isCorrect;
24092
- if (userInput.noneOfTheAboveIndex === i) {
24093
- isCorrect = rubric.choices.every((choice, j) => {
24094
- return i === j || !choice.correct;
24095
- });
24096
- } else {
24097
- isCorrect = !!rubric.choices[i].correct;
24098
- }
24099
- return isCorrect === selected;
24100
- });
24101
- return {
24102
- type: "points",
24103
- earned: correct ? 1 : 0,
24104
- total: 1,
24105
- message: null
24106
- };
24079
+ return radioValidator(userInput, rubric, strings);
24107
24080
  }
24108
24081
  static getUserInputFromProps(props) {
24109
24082
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
@@ -24192,6 +24165,12 @@ class Radio$1 extends React.Component {
24192
24165
  registerFocusFunction(fun) {
24193
24166
  this.focusFunction = fun;
24194
24167
  }
24168
+ getUserInput() {
24169
+ return Radio$1.getUserInputFromProps(this.props);
24170
+ }
24171
+ simpleValidate(rubric) {
24172
+ return radioValidator(this.getUserInput(), rubric, this.context.strings);
24173
+ }
24195
24174
  render() {
24196
24175
  const {
24197
24176
  choices
@@ -24468,12 +24447,6 @@ class Categorizer extends React.Component {
24468
24447
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
24469
24448
  return change.apply(this, args);
24470
24449
  };
24471
- this.getUserInput = () => {
24472
- return Categorizer.getUserInputFromProps(this.props);
24473
- };
24474
- this.simpleValidate = rubric => {
24475
- return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24476
- };
24477
24450
  }
24478
24451
  static validate(userInput, rubric, strings) {
24479
24452
  return categorizerValidator(userInput, rubric, strings);
@@ -24483,6 +24456,9 @@ class Categorizer extends React.Component {
24483
24456
  values: props.values
24484
24457
  };
24485
24458
  }
24459
+ getUserInput() {
24460
+ return Categorizer.getUserInputFromProps(this.props);
24461
+ }
24486
24462
  onChange(itemNum, catNum) {
24487
24463
  const values = [...this.props.values];
24488
24464
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -24490,6 +24466,9 @@ class Categorizer extends React.Component {
24490
24466
  this.change("values", values);
24491
24467
  this.props.trackInteraction();
24492
24468
  }
24469
+ simpleValidate(rubric) {
24470
+ return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24471
+ }
24493
24472
  render() {
24494
24473
  const self = this;
24495
24474
 
@@ -24685,6 +24664,31 @@ const parseUrl = url => new URL(url, location().origin);
24685
24664
  */
24686
24665
  const toAbsoluteUrl = url => parseUrl(url).href;
24687
24666
 
24667
+ function csProgramValidator(state) {
24668
+ // The iframe can tell us whether it's correct or incorrect,
24669
+ // and pass an optional message
24670
+ if (state.status === "correct") {
24671
+ return {
24672
+ type: "points",
24673
+ earned: 1,
24674
+ total: 1,
24675
+ message: state.message || null
24676
+ };
24677
+ }
24678
+ if (state.status === "incorrect") {
24679
+ return {
24680
+ type: "points",
24681
+ earned: 0,
24682
+ total: 1,
24683
+ message: state.message || null
24684
+ };
24685
+ }
24686
+ return {
24687
+ type: "invalid",
24688
+ message: "Keep going, you're not there yet!"
24689
+ };
24690
+ }
24691
+
24688
24692
  /**
24689
24693
  * This widget is for embedding Khan Academy CS programs.
24690
24694
  */
@@ -24735,29 +24739,8 @@ class CSProgram extends React.Component {
24735
24739
  };
24736
24740
  }
24737
24741
  // The widget's grading function
24738
- static validate(state, rubric) {
24739
- // The iframe can tell us whether it's correct or incorrect,
24740
- // and pass an optional message
24741
- if (state.status === "correct") {
24742
- return {
24743
- type: "points",
24744
- earned: 1,
24745
- total: 1,
24746
- message: state.message || null
24747
- };
24748
- }
24749
- if (state.status === "incorrect") {
24750
- return {
24751
- type: "points",
24752
- earned: 0,
24753
- total: 1,
24754
- message: state.message || null
24755
- };
24756
- }
24757
- return {
24758
- type: "invalid",
24759
- message: "Keep going, you're not there yet!"
24760
- };
24742
+ static validate(state) {
24743
+ return csProgramValidator(state);
24761
24744
  }
24762
24745
  componentDidMount() {
24763
24746
  $(window).on("message", this.handleMessageEvent);
@@ -24765,11 +24748,11 @@ class CSProgram extends React.Component {
24765
24748
  componentWillUnmount() {
24766
24749
  $(window).off("message", this.handleMessageEvent);
24767
24750
  }
24768
- simpleValidate(rubric) {
24769
- return CSProgram.validate({
24751
+ simpleValidate() {
24752
+ return csProgramValidator({
24770
24753
  status: this.props.status,
24771
24754
  message: this.props.message
24772
- }, rubric);
24755
+ });
24773
24756
  }
24774
24757
  render() {
24775
24758
  if (!this.props.programID) {
@@ -24856,16 +24839,14 @@ var CSProgram$1 = {
24856
24839
  };
24857
24840
 
24858
24841
  class Definition extends React.Component {
24859
- constructor(...args) {
24860
- super(...args);
24861
- this.getUserInput = () => {
24862
- return {};
24863
- };
24864
- }
24865
24842
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24866
24843
  static validate() {
24867
24844
  return noopValidator();
24868
24845
  }
24846
+ getUserInput() {
24847
+ return {};
24848
+ }
24849
+
24869
24850
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24870
24851
  simpleValidate() {
24871
24852
  return noopValidator();
@@ -25002,18 +24983,18 @@ class Dropdown extends React.Component {
25002
24983
  selected: selected
25003
24984
  });
25004
24985
  };
25005
- this.getUserInput = () => {
25006
- return {
25007
- value: this.props.selected
25008
- };
25009
- };
25010
- this.simpleValidate = rubric => {
25011
- return dropdownValidator(this.getUserInput(), rubric);
25012
- };
25013
24986
  }
25014
24987
  static validate(userInput, rubric) {
25015
24988
  return dropdownValidator(userInput, rubric);
25016
24989
  }
24990
+ getUserInput() {
24991
+ return {
24992
+ value: this.props.selected
24993
+ };
24994
+ }
24995
+ simpleValidate(rubric) {
24996
+ return dropdownValidator(this.getUserInput(), rubric);
24997
+ }
25017
24998
  render() {
25018
24999
  const children = [/*#__PURE__*/React.createElement(OptionItem, {
25019
25000
  key: "placeholder",
@@ -25087,9 +25068,6 @@ class Explanation extends React.Component {
25087
25068
  });
25088
25069
  this.props.trackInteraction();
25089
25070
  };
25090
- this.getUserInput = () => {
25091
- return {};
25092
- };
25093
25071
  }
25094
25072
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25095
25073
  static validate() {
@@ -25101,6 +25079,10 @@ class Explanation extends React.Component {
25101
25079
  componentWillUnmount() {
25102
25080
  this._mounted = false;
25103
25081
  }
25082
+ getUserInput() {
25083
+ return {};
25084
+ }
25085
+
25104
25086
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25105
25087
  simpleValidate() {
25106
25088
  return noopValidator();
@@ -26361,49 +26343,6 @@ function functionForType(
26361
26343
  type) {
26362
26344
  return functionTypeMapping[type];
26363
26345
  }
26364
- const validate$1 = (state, rubric) => {
26365
- if (state.type !== rubric.correct.type) {
26366
- return {
26367
- type: "points",
26368
- earned: 0,
26369
- total: 1,
26370
- message: null
26371
- };
26372
- }
26373
-
26374
- // We haven't moved the coords
26375
- if (state.coords == null) {
26376
- return {
26377
- type: "invalid",
26378
- message: null
26379
- };
26380
- }
26381
-
26382
- // Get new function handler for grading
26383
- const grader = functionForType(state.type);
26384
- const guessCoeffs = grader.getCoefficients(state.coords, state.asymptote);
26385
- const correctCoeffs = grader.getCoefficients(rubric.correct.coords, rubric.correct.asymptote);
26386
- if (guessCoeffs == null || correctCoeffs == null) {
26387
- return {
26388
- type: "invalid",
26389
- message: null
26390
- };
26391
- }
26392
- if (grader.areEqual(guessCoeffs, correctCoeffs)) {
26393
- return {
26394
- type: "points",
26395
- earned: 1,
26396
- total: 1,
26397
- message: null
26398
- };
26399
- }
26400
- return {
26401
- type: "points",
26402
- earned: 0,
26403
- total: 1,
26404
- message: null
26405
- };
26406
- };
26407
26346
  const getEquationString = props => {
26408
26347
  const plot = props.plot;
26409
26348
  if (plot.type && plot.coords) {
@@ -26521,7 +26460,6 @@ var util = /*#__PURE__*/Object.freeze({
26521
26460
  DEFAULT_BACKGROUND_IMAGE: DEFAULT_BACKGROUND_IMAGE,
26522
26461
  allTypes: allTypes,
26523
26462
  functionForType: functionForType,
26524
- validate: validate$1,
26525
26463
  getEquationString: getEquationString,
26526
26464
  pointsFromNormalized: pointsFromNormalized,
26527
26465
  maybePointsFromNormalized: maybePointsFromNormalized,
@@ -26532,6 +26470,52 @@ var util = /*#__PURE__*/Object.freeze({
26532
26470
  typeToButton: typeToButton
26533
26471
  });
26534
26472
 
26473
+ function grapherValidator(state, rubric) {
26474
+ if (state.type !== rubric.correct.type) {
26475
+ return {
26476
+ type: "points",
26477
+ earned: 0,
26478
+ total: 1,
26479
+ message: null
26480
+ };
26481
+ }
26482
+
26483
+ // We haven't moved the coords
26484
+ if (state.coords == null) {
26485
+ return {
26486
+ type: "invalid",
26487
+ message: null
26488
+ };
26489
+ }
26490
+
26491
+ // Get new function handler for grading
26492
+ const grader = functionForType(state.type);
26493
+ const guessCoeffs = grader.getCoefficients(state.coords, state.asymptote);
26494
+ const correctCoeffs = grader.getCoefficients(rubric.correct.coords,
26495
+ // @ts-expect-error - TS(2339) - Property 'asymptote' does not exist on type '{ type: "absolute_value"; coords: [Coord, Coord]; }'
26496
+ rubric.correct.asymptote);
26497
+ if (guessCoeffs == null || correctCoeffs == null) {
26498
+ return {
26499
+ type: "invalid",
26500
+ message: null
26501
+ };
26502
+ }
26503
+ if (grader.areEqual(guessCoeffs, correctCoeffs)) {
26504
+ return {
26505
+ type: "points",
26506
+ earned: 1,
26507
+ total: 1,
26508
+ message: null
26509
+ };
26510
+ }
26511
+ return {
26512
+ type: "points",
26513
+ earned: 0,
26514
+ total: 1,
26515
+ message: null
26516
+ };
26517
+ }
26518
+
26535
26519
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
26536
26520
  const MovablePoint$3 = Graphie.MovablePoint;
26537
26521
  // @ts-expect-error - TS2339 - Property 'MovableLine' does not exist on type 'typeof Graphie'.
@@ -26724,7 +26708,7 @@ FunctionGrapher.defaultProps = {
26724
26708
  coords: null,
26725
26709
  asymptote: null,
26726
26710
  isMobile: false
26727
- }; // Really this is props.plot, I think
26711
+ };
26728
26712
  /* Widget and editor. */
26729
26713
  class Grapher extends React.Component {
26730
26714
  constructor(...args) {
@@ -26817,7 +26801,7 @@ class Grapher extends React.Component {
26817
26801
  };
26818
26802
  }
26819
26803
  static validate(state, rubric) {
26820
- return validate$1(state, rubric);
26804
+ return grapherValidator(state, rubric);
26821
26805
  }
26822
26806
  static getUserInputFromProps(props) {
26823
26807
  return props.plot;
@@ -26838,7 +26822,7 @@ class Grapher extends React.Component {
26838
26822
  return tickStep;
26839
26823
  }
26840
26824
  simpleValidate(rubric) {
26841
- return validate$1(this.getUserInput(), rubric);
26825
+ return grapherValidator(this.getUserInput(), rubric);
26842
26826
  }
26843
26827
  getUserInput() {
26844
26828
  return Grapher.getUserInputFromProps(this.props);
@@ -26940,49 +26924,41 @@ class Group extends React.Component {
26940
26924
  this.change = (...args) => {
26941
26925
  return change.apply(this, args);
26942
26926
  };
26943
- this.getUserInput = () => {
26944
- var _this$rendererRef;
26945
- return (_this$rendererRef = this.rendererRef) == null ? void 0 : _this$rendererRef.getUserInput();
26946
- };
26947
26927
  this.getSerializedState = () => {
26948
- var _this$rendererRef2;
26949
- return (_this$rendererRef2 = this.rendererRef) == null ? void 0 : _this$rendererRef2.getSerializedState();
26928
+ var _this$rendererRef;
26929
+ return (_this$rendererRef = this.rendererRef) == null ? void 0 : _this$rendererRef.getSerializedState();
26950
26930
  };
26951
26931
  this.restoreSerializedState = (state, callback) => {
26952
- var _this$rendererRef3;
26953
- (_this$rendererRef3 = this.rendererRef) == null || _this$rendererRef3.restoreSerializedState(state, callback);
26932
+ var _this$rendererRef2;
26933
+ (_this$rendererRef2 = this.rendererRef) == null || _this$rendererRef2.restoreSerializedState(state, callback);
26954
26934
  // Tell our renderer that we have no props to change
26955
26935
  // (all our changes were in state):
26956
26936
  return null;
26957
26937
  };
26958
- this.simpleValidate = rubric => {
26959
- var _this$rendererRef4;
26960
- return (_this$rendererRef4 = this.rendererRef) == null ? void 0 : _this$rendererRef4.score();
26961
- };
26962
26938
  // Mobile API:
26963
26939
  this.getInputPaths = () => {
26964
- var _this$rendererRef5;
26965
- return (_this$rendererRef5 = this.rendererRef) == null ? void 0 : _this$rendererRef5.getInputPaths();
26940
+ var _this$rendererRef3;
26941
+ return (_this$rendererRef3 = this.rendererRef) == null ? void 0 : _this$rendererRef3.getInputPaths();
26966
26942
  };
26967
26943
  this.setInputValue = (path, newValue, callback) => {
26968
- var _this$rendererRef6;
26969
- return (_this$rendererRef6 = this.rendererRef) == null ? void 0 : _this$rendererRef6.setInputValue(path, newValue, callback);
26944
+ var _this$rendererRef4;
26945
+ return (_this$rendererRef4 = this.rendererRef) == null ? void 0 : _this$rendererRef4.setInputValue(path, newValue, callback);
26970
26946
  };
26971
26947
  this.focus = () => {
26972
- var _this$rendererRef7;
26973
- return (_this$rendererRef7 = this.rendererRef) == null ? void 0 : _this$rendererRef7.focus();
26948
+ var _this$rendererRef5;
26949
+ return (_this$rendererRef5 = this.rendererRef) == null ? void 0 : _this$rendererRef5.focus();
26974
26950
  };
26975
26951
  this.focusInputPath = path => {
26976
- var _this$rendererRef8;
26977
- (_this$rendererRef8 = this.rendererRef) == null || _this$rendererRef8.focusPath(path);
26952
+ var _this$rendererRef6;
26953
+ (_this$rendererRef6 = this.rendererRef) == null || _this$rendererRef6.focusPath(path);
26978
26954
  };
26979
26955
  this.blurInputPath = path => {
26980
- var _this$rendererRef9;
26981
- (_this$rendererRef9 = this.rendererRef) == null || _this$rendererRef9.blurPath(path);
26956
+ var _this$rendererRef7;
26957
+ (_this$rendererRef7 = this.rendererRef) == null || _this$rendererRef7.blurPath(path);
26982
26958
  };
26983
26959
  this.showRationalesForCurrentlySelectedChoices = () => {
26984
- var _this$rendererRef10;
26985
- (_this$rendererRef10 = this.rendererRef) == null || _this$rendererRef10.showRationalesForCurrentlySelectedChoices();
26960
+ var _this$rendererRef8;
26961
+ (_this$rendererRef8 = this.rendererRef) == null || _this$rendererRef8.showRationalesForCurrentlySelectedChoices();
26986
26962
  };
26987
26963
  }
26988
26964
  componentDidMount() {
@@ -26991,6 +26967,14 @@ class Group extends React.Component {
26991
26967
  // the group with the correct number.
26992
26968
  this.forceUpdate();
26993
26969
  }
26970
+ getUserInput() {
26971
+ var _this$rendererRef9;
26972
+ return (_this$rendererRef9 = this.rendererRef) == null ? void 0 : _this$rendererRef9.getUserInput();
26973
+ }
26974
+ simpleValidate() {
26975
+ var _this$rendererRef10;
26976
+ return (_this$rendererRef10 = this.rendererRef) == null ? void 0 : _this$rendererRef10.score();
26977
+ }
26994
26978
  render() {
26995
26979
  var _this$rendererRef11;
26996
26980
  const apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
@@ -27079,6 +27063,31 @@ var Group$1 = {
27079
27063
  isLintable: true
27080
27064
  };
27081
27065
 
27066
+ function iframeValidator(state) {
27067
+ // The iframe can tell us whether it's correct or incorrect,
27068
+ // and pass an optional message
27069
+ if (state.status === "correct") {
27070
+ return {
27071
+ type: "points",
27072
+ earned: 1,
27073
+ total: 1,
27074
+ message: state.message || null
27075
+ };
27076
+ }
27077
+ if (state.status === "incorrect") {
27078
+ return {
27079
+ type: "points",
27080
+ earned: 0,
27081
+ total: 1,
27082
+ message: state.message || null
27083
+ };
27084
+ }
27085
+ return {
27086
+ type: "invalid",
27087
+ message: "Keep going, you're not there yet!"
27088
+ };
27089
+ }
27090
+
27082
27091
  /**
27083
27092
  * This is an iframe widget. It is used for rendering an iframe that
27084
27093
  * then communicates its state via window.postMessage
@@ -27117,10 +27126,9 @@ class Iframe extends React.Component {
27117
27126
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
27118
27127
  return change.apply(this, args);
27119
27128
  };
27120
- this.simpleValidate = rubric => {
27121
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Iframe'.
27122
- return Iframe.validate(this.getUserInput(), rubric);
27123
- };
27129
+ }
27130
+ static validate(state) {
27131
+ return iframeValidator(state);
27124
27132
  }
27125
27133
  componentDidMount() {
27126
27134
  $(window).on("message", this.handleMessageEvent);
@@ -27134,6 +27142,9 @@ class Iframe extends React.Component {
27134
27142
  message: this.props.message
27135
27143
  };
27136
27144
  }
27145
+ simpleValidate() {
27146
+ return iframeValidator(this.getUserInput());
27147
+ }
27137
27148
  render() {
27138
27149
  const style = {
27139
27150
  width: String(this.props.width),
@@ -27195,10 +27206,6 @@ class Iframe extends React.Component {
27195
27206
  });
27196
27207
  }
27197
27208
  }
27198
-
27199
- /**
27200
- * This is the widget's grading function
27201
- */
27202
27209
  Iframe.defaultProps = {
27203
27210
  status: "incomplete",
27204
27211
  // optional message
@@ -27206,32 +27213,6 @@ Iframe.defaultProps = {
27206
27213
  allowFullScreen: false,
27207
27214
  allowTopNavigation: false
27208
27215
  };
27209
- _.extend(Iframe, {
27210
- validate: function (state, rubric) {
27211
- // The iframe can tell us whether it's correct or incorrect,
27212
- // and pass an optional message
27213
- if (state.status === "correct") {
27214
- return {
27215
- type: "points",
27216
- earned: 1,
27217
- total: 1,
27218
- message: state.message || null
27219
- };
27220
- }
27221
- if (state.status === "incorrect") {
27222
- return {
27223
- type: "points",
27224
- earned: 0,
27225
- total: 1,
27226
- message: state.message || null
27227
- };
27228
- }
27229
- return {
27230
- type: "invalid",
27231
- message: "Keep going, you're not there yet!"
27232
- };
27233
- }
27234
- });
27235
27216
  var Iframe$1 = {
27236
27217
  name: "iframe",
27237
27218
  displayName: "Iframe (deprecated)",
@@ -27258,15 +27239,16 @@ class ImageWidget extends React.Component {
27258
27239
  this.change = (...args) => {
27259
27240
  return change.apply(this, args);
27260
27241
  };
27261
- this.getUserInput = () => {
27262
- return null;
27263
- };
27264
27242
  this.focus = () => {};
27265
27243
  }
27266
27244
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27267
27245
  static validate() {
27268
27246
  return noopValidator();
27269
27247
  }
27248
+ getUserInput() {
27249
+ return null;
27250
+ }
27251
+
27270
27252
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27271
27253
  simpleValidate() {
27272
27254
  return noopValidator();
@@ -27578,13 +27560,6 @@ class Interaction extends React.Component {
27578
27560
  this.change = (...args) => {
27579
27561
  return change.apply(this, args);
27580
27562
  };
27581
- this.getUserInput = () => {
27582
- // TODO(eater): Perhaps we want to be able to record the state of the
27583
- // user's interaction. Unfortunately sending all the props will
27584
- // probably make the attempt payload too large. So for now, don't send
27585
- // anything.
27586
- return {};
27587
- };
27588
27563
  }
27589
27564
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27590
27565
  static validate() {
@@ -27608,6 +27583,14 @@ class Interaction extends React.Component {
27608
27583
  });
27609
27584
  }
27610
27585
  }
27586
+ getUserInput() {
27587
+ // TODO(eater): Perhaps we want to be able to record the state of the
27588
+ // user's interaction. Unfortunately sending all the props will
27589
+ // probably make the attempt payload too large. So for now, don't send
27590
+ // anything.
27591
+ return {};
27592
+ }
27593
+
27611
27594
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27612
27595
  simpleValidate() {
27613
27596
  return noopValidator();
@@ -28736,7 +28719,8 @@ const tickSize = 10;
28736
28719
  const tickLabelSize = 14;
28737
28720
  const YGridTick = ({
28738
28721
  y,
28739
- range
28722
+ range,
28723
+ tickStep
28740
28724
  }) => {
28741
28725
  // If the graph requires out-of-bounds labels, we want to make sure to set the
28742
28726
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -28771,9 +28755,10 @@ const YGridTick = ({
28771
28755
 
28772
28756
  // If the graph displays both the y and x axis lines within the graph, we want
28773
28757
  // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
28774
- const showLabel = shouldShowLabel(y, range);
28758
+ const showLabel = shouldShowLabel(y, range, tickStep);
28775
28759
  return /*#__PURE__*/React.createElement("g", {
28776
- className: "tick"
28760
+ className: "tick",
28761
+ "aria-hidden": true
28777
28762
  }, /*#__PURE__*/React.createElement("line", {
28778
28763
  x1: x1,
28779
28764
  y1: y1,
@@ -28832,7 +28817,8 @@ const XGridTick = ({
28832
28817
  const xPositionText = xPosition + xAdjustment;
28833
28818
  const yPositionText = yPosition + yAdjustment;
28834
28819
  return /*#__PURE__*/React.createElement("g", {
28835
- className: "tick"
28820
+ className: "tick",
28821
+ "aria-hidden": true
28836
28822
  }, /*#__PURE__*/React.createElement("line", {
28837
28823
  x1: x1,
28838
28824
  y1: y1,
@@ -28851,13 +28837,13 @@ const XGridTick = ({
28851
28837
  };
28852
28838
 
28853
28839
  // Determines whether to show the label for the given tick
28854
- // Currently, the only condition is to hide the label at -1
28855
- // on the y-axis when the x-axis is within the graph
28856
- const shouldShowLabel = (number, range) => {
28840
+ // Currently, the only condition is to hide the label at -tickStep
28841
+ // on the y-axis when the y-axis is within the graph bounds
28842
+ const shouldShowLabel = (currentTick, range, tickStep) => {
28857
28843
  let showLabel = true;
28858
28844
 
28859
- // If the x-axis is within the graph and the y-axis is at -1, hide the label
28860
- if (range[X][MIN] < -1 && range[X][MAX] > 0 && number === -1) {
28845
+ // If the y-axis is within the graph and currentTick equals -tickStep, hide the label
28846
+ if (range[X][MIN] < -tickStep && range[X][MAX] > 0 && currentTick === -tickStep) {
28861
28847
  showLabel = false;
28862
28848
  }
28863
28849
  return showLabel;
@@ -28899,7 +28885,8 @@ const AxisTicks = () => {
28899
28885
  return /*#__PURE__*/React.createElement(YGridTick, {
28900
28886
  y: y,
28901
28887
  key: `y-grid-tick-${y}`,
28902
- range: range
28888
+ range: range,
28889
+ tickStep: tickStep[Y]
28903
28890
  });
28904
28891
  })), /*#__PURE__*/React.createElement("g", {
28905
28892
  className: "x-axis-ticks"
@@ -29652,7 +29639,8 @@ function LockedLabel(props) {
29652
29639
  color: lockedFigureColors[color],
29653
29640
  fontSize: font.size[size],
29654
29641
  backgroundColor: "rgba(255, 255, 255, 0.8)"
29655
- }
29642
+ },
29643
+ "aria-hidden": true
29656
29644
  }, /*#__PURE__*/React.createElement(TeX, null, text));
29657
29645
  }
29658
29646
 
@@ -29662,7 +29650,7 @@ function GraphLockedLabelsLayer(props) {
29662
29650
  lockedFigures
29663
29651
  } = props;
29664
29652
  return lockedFigures.map((figure, i) => {
29665
- var _flags$mafs, _flags$mafs2;
29653
+ var _flags$mafs, _flags$mafs2, _flags$mafs3, _flags$mafs4, _flags$mafs5, _flags$mafs6;
29666
29654
  if (figure.type === "label") {
29667
29655
  return /*#__PURE__*/React.createElement(LockedLabel, _extends({
29668
29656
  key: `label-${i}`
@@ -29672,14 +29660,23 @@ function GraphLockedLabelsLayer(props) {
29672
29660
  // Point flag + point type
29673
29661
  flags != null && (_flags$mafs = flags["mafs"]) != null && _flags$mafs["locked-point-labels"] && figure.type === "point" ||
29674
29662
  // Line flag + line type
29675
- flags != null && (_flags$mafs2 = flags["mafs"]) != null && _flags$mafs2["locked-line-labels"] && figure.type === "line") {
29663
+ flags != null && (_flags$mafs2 = flags["mafs"]) != null && _flags$mafs2["locked-line-labels"] && figure.type === "line" ||
29664
+ // Vector flag + vector type
29665
+ flags != null && (_flags$mafs3 = flags["mafs"]) != null && _flags$mafs3["locked-vector-labels"] && figure.type === "vector" ||
29666
+ // Ellipse flag + ellipse type
29667
+ flags != null && (_flags$mafs4 = flags["mafs"]) != null && _flags$mafs4["locked-ellipse-labels"] && figure.type === "ellipse" ||
29668
+ // Polygon flag + polygon type
29669
+ flags != null && (_flags$mafs5 = flags["mafs"]) != null && _flags$mafs5["locked-polygon-labels"] && figure.type === "polygon" ||
29670
+ // Function flag + function type
29671
+ flags != null && (_flags$mafs6 = flags["mafs"]) != null && _flags$mafs6["locked-function-labels"] && figure.type === "function") {
29672
+ var _figure$labels, _figure$points$0$labe, _figure$points$1$labe;
29676
29673
  return /*#__PURE__*/React.createElement(React.Fragment, {
29677
29674
  key: i
29678
- }, figure.labels.map((label, j) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29675
+ }, (_figure$labels = figure.labels) == null ? void 0 : _figure$labels.map((label, j) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29679
29676
  key: `${i}-label-${j}`
29680
- }, label))), figure.type === "line" && /*#__PURE__*/React.createElement(React.Fragment, null, figure.points[0].labels.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29677
+ }, label))), figure.type === "line" && /*#__PURE__*/React.createElement(React.Fragment, null, (_figure$points$0$labe = figure.points[0].labels) == null ? void 0 : _figure$points$0$labe.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29681
29678
  key: `locked-figure-${i}-point-1-label-${k}`
29682
- }, label))), figure.points[1].labels.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29679
+ }, label))), (_figure$points$1$labe = figure.points[1].labels) == null ? void 0 : _figure$points$1$labe.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29683
29680
  key: `locked-figure-${i}-point-2-label-${k}`
29684
29681
  }, label)))));
29685
29682
  }
@@ -29688,15 +29685,23 @@ function GraphLockedLabelsLayer(props) {
29688
29685
  }
29689
29686
 
29690
29687
  const LockedEllipse = props => {
29688
+ var _flags$mafs;
29691
29689
  const {
29692
29690
  center,
29693
29691
  radius,
29694
29692
  angle,
29695
29693
  color: color$1,
29696
29694
  fillStyle,
29697
- strokeStyle
29695
+ strokeStyle,
29696
+ ariaLabel,
29697
+ flags
29698
29698
  } = props;
29699
- return /*#__PURE__*/React.createElement(Ellipse, {
29699
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29700
+ return /*#__PURE__*/React.createElement("g", {
29701
+ className: "locked-ellipse",
29702
+ "aria-label": hasAria ? ariaLabel : undefined,
29703
+ "aria-hidden": !hasAria
29704
+ }, /*#__PURE__*/React.createElement(Ellipse, {
29700
29705
  center: center,
29701
29706
  radius: radius,
29702
29707
  angle: angle,
@@ -29712,10 +29717,11 @@ const LockedEllipse = props => {
29712
29717
  fill: fillStyle === "white" ? color.white : lockedFigureColors[color$1]
29713
29718
  }
29714
29719
  }
29715
- });
29720
+ }));
29716
29721
  };
29717
29722
 
29718
29723
  const LockedFunction = props => {
29724
+ var _props$flags;
29719
29725
  const [equation, setEquation] = useState();
29720
29726
  const {
29721
29727
  color,
@@ -29728,6 +29734,7 @@ const LockedFunction = props => {
29728
29734
  style: strokeStyle,
29729
29735
  domain
29730
29736
  };
29737
+ const hasAria = props.ariaLabel && ((_props$flags = props.flags) == null || (_props$flags = _props$flags["mafs"]) == null ? void 0 : _props$flags["locked-figures-aria"]);
29731
29738
  useEffect(() => {
29732
29739
  // Parsing the equation in a "useEffect" hook saves about 2ms each frame
29733
29740
  // when the learner is interacting with the graph (i.e. moving points).
@@ -29737,7 +29744,9 @@ const LockedFunction = props => {
29737
29744
  return null;
29738
29745
  }
29739
29746
  return /*#__PURE__*/React.createElement("g", {
29740
- className: "locked-function"
29747
+ className: "locked-function",
29748
+ "aria-label": hasAria ? props.ariaLabel : undefined,
29749
+ "aria-hidden": !hasAria
29741
29750
  }, directionalAxis === "x" && /*#__PURE__*/React.createElement(Plot$3.OfX, _extends({
29742
29751
  y: x => equation.eval({
29743
29752
  x
@@ -29830,6 +29839,7 @@ function isBetween(x, low, high) {
29830
29839
  }
29831
29840
 
29832
29841
  const LockedLine = props => {
29842
+ var _flags$mafs;
29833
29843
  const {
29834
29844
  color: color$1,
29835
29845
  lineStyle,
@@ -29837,9 +29847,12 @@ const LockedLine = props => {
29837
29847
  points,
29838
29848
  showPoint1,
29839
29849
  showPoint2,
29850
+ ariaLabel,
29851
+ flags,
29840
29852
  range
29841
29853
  } = props;
29842
29854
  const [point1, point2] = points;
29855
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29843
29856
  let line;
29844
29857
  if (kind === "ray") {
29845
29858
  // Rays extend to the end of the graph in one direction.
@@ -29879,7 +29892,9 @@ const LockedLine = props => {
29879
29892
  }), endArrowHead);
29880
29893
  }
29881
29894
  return /*#__PURE__*/React.createElement("g", {
29882
- className: kind === "ray" ? "locked-ray" : "locked-line"
29895
+ className: kind === "ray" ? "locked-ray" : "locked-line",
29896
+ "aria-label": hasAria ? ariaLabel : undefined,
29897
+ "aria-hidden": !hasAria
29883
29898
  }, line, showPoint1 && /*#__PURE__*/React.createElement(Point$2, {
29884
29899
  x: point1.coord[X],
29885
29900
  y: point1.coord[Y],
@@ -29904,13 +29919,21 @@ const LockedLine = props => {
29904
29919
  };
29905
29920
 
29906
29921
  const LockedPoint = props => {
29922
+ var _flags$mafs;
29907
29923
  const {
29924
+ flags,
29908
29925
  color: color$1,
29909
29926
  coord,
29910
- filled
29927
+ filled,
29928
+ ariaLabel
29911
29929
  } = props;
29912
29930
  const [x, y] = coord;
29913
- return /*#__PURE__*/React.createElement(Point$2, {
29931
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29932
+ return /*#__PURE__*/React.createElement("g", {
29933
+ className: "locked-point",
29934
+ "aria-label": hasAria ? ariaLabel : undefined,
29935
+ "aria-hidden": !hasAria
29936
+ }, /*#__PURE__*/React.createElement(Point$2, {
29914
29937
  x: x,
29915
29938
  y: y,
29916
29939
  svgCircleProps: {
@@ -29920,10 +29943,11 @@ const LockedPoint = props => {
29920
29943
  strokeWidth: spacing.xxxxSmall_2
29921
29944
  }
29922
29945
  }
29923
- });
29946
+ }));
29924
29947
  };
29925
29948
 
29926
29949
  const LockedPolygon = props => {
29950
+ var _props$flags;
29927
29951
  const {
29928
29952
  points,
29929
29953
  color: color$1,
@@ -29931,8 +29955,11 @@ const LockedPolygon = props => {
29931
29955
  fillStyle,
29932
29956
  strokeStyle
29933
29957
  } = props;
29958
+ const hasAria = props.ariaLabel && ((_props$flags = props.flags) == null || (_props$flags = _props$flags["mafs"]) == null ? void 0 : _props$flags["locked-figures-aria"]);
29934
29959
  return /*#__PURE__*/React.createElement("g", {
29935
- className: "locked-polygon"
29960
+ className: "locked-polygon",
29961
+ "aria-label": hasAria ? props.ariaLabel : undefined,
29962
+ "aria-hidden": !hasAria
29936
29963
  }, /*#__PURE__*/React.createElement(Polygon, {
29937
29964
  points: [...points],
29938
29965
  fillOpacity: lockedFigureFillStyles[fillStyle],
@@ -29956,13 +29983,19 @@ const LockedPolygon = props => {
29956
29983
  };
29957
29984
 
29958
29985
  const LockedVector = props => {
29986
+ var _flags$mafs;
29959
29987
  const {
29960
29988
  color,
29961
- points
29989
+ points,
29990
+ ariaLabel,
29991
+ flags
29962
29992
  } = props;
29963
29993
  const [tail, tip] = points;
29994
+ const hasAria = ariaLabel && (flags == null || (_flags$mafs = flags["mafs"]) == null ? void 0 : _flags$mafs["locked-figures-aria"]);
29964
29995
  return /*#__PURE__*/React.createElement("g", {
29965
- className: "locked-vector"
29996
+ className: "locked-vector",
29997
+ "aria-label": hasAria ? ariaLabel : undefined,
29998
+ "aria-hidden": !hasAria
29966
29999
  }, /*#__PURE__*/React.createElement(Vector, {
29967
30000
  tail: tail,
29968
30001
  tip: tip,
@@ -29972,6 +30005,7 @@ const LockedVector = props => {
29972
30005
 
29973
30006
  const GraphLockedLayer = props => {
29974
30007
  const {
30008
+ flags,
29975
30009
  lockedFigures
29976
30010
  } = props;
29977
30011
  return /*#__PURE__*/React.createElement(React.Fragment, null, lockedFigures.map((figure, index) => {
@@ -29979,28 +30013,40 @@ const GraphLockedLayer = props => {
29979
30013
  case "point":
29980
30014
  return /*#__PURE__*/React.createElement(LockedPoint, _extends({
29981
30015
  key: `point-${index}`
29982
- }, figure));
30016
+ }, figure, {
30017
+ flags: flags
30018
+ }));
29983
30019
  case "line":
29984
30020
  return /*#__PURE__*/React.createElement(LockedLine, _extends({
29985
30021
  key: `line-${index}`,
29986
30022
  range: props.range
29987
- }, figure));
30023
+ }, figure, {
30024
+ flags: flags
30025
+ }));
29988
30026
  case "vector":
29989
30027
  return /*#__PURE__*/React.createElement(LockedVector, _extends({
29990
30028
  key: `vector-${index}`
29991
- }, figure));
30029
+ }, figure, {
30030
+ flags: flags
30031
+ }));
29992
30032
  case "ellipse":
29993
30033
  return /*#__PURE__*/React.createElement(LockedEllipse, _extends({
29994
30034
  key: `ellipse-${index}`
29995
- }, figure));
30035
+ }, figure, {
30036
+ flags: flags
30037
+ }));
29996
30038
  case "polygon":
29997
30039
  return /*#__PURE__*/React.createElement(LockedPolygon, _extends({
29998
30040
  key: `polygon-${index}`
29999
- }, figure));
30041
+ }, figure, {
30042
+ flags: flags
30043
+ }));
30000
30044
  case "function":
30001
30045
  return /*#__PURE__*/React.createElement(LockedFunction, _extends({
30002
30046
  key: `function-${index}`
30003
- }, figure));
30047
+ }, figure, {
30048
+ flags: flags
30049
+ }));
30004
30050
  case "label":
30005
30051
  // This is rendered outside the SVG element, since
30006
30052
  // TeX cannot be rendered inside an SVG.
@@ -31466,7 +31512,7 @@ function QuadraticGraph(props) {
31466
31512
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
31467
31513
  // While we should technically never have invalid coordinates, we want to ensure that we have a fallback
31468
31514
  const coeffRef = React.useRef([0, 0, 0]);
31469
- const coeffs = getQuadraticCoefficients(coords);
31515
+ const coeffs = getQuadraticCoefficients$1(coords);
31470
31516
  if (coeffs !== undefined) {
31471
31517
  coeffRef.current = coeffs;
31472
31518
  }
@@ -31490,7 +31536,7 @@ function QuadraticGraph(props) {
31490
31536
  // These equations were originally set up in 2013 and may require some
31491
31537
  // additional comments to help clarify the quadratic formula manipulations
31492
31538
  // Origin: https://phabricator.khanacademy.org/D2413
31493
- const getQuadraticCoefficients = coords => {
31539
+ const getQuadraticCoefficients$1 = coords => {
31494
31540
  const p1 = coords[0];
31495
31541
  const p2 = coords[1];
31496
31542
  const p3 = coords[2];
@@ -31530,7 +31576,7 @@ function SinusoidGraph(props) {
31530
31576
  phase: 1,
31531
31577
  verticalOffset: 0
31532
31578
  });
31533
- const coeffs = getSinusoidCoefficients(coords);
31579
+ const coeffs = getSinusoidCoefficients$1(coords);
31534
31580
 
31535
31581
  // If the coefficients are valid, update the reference
31536
31582
  if (coeffs !== undefined) {
@@ -31559,7 +31605,7 @@ sinusoidCoefficients) {
31559
31605
  } = sinusoidCoefficients;
31560
31606
  return a * Math.sin(b * x - c) + d;
31561
31607
  };
31562
- const getSinusoidCoefficients = coords => {
31608
+ const getSinusoidCoefficients$1 = coords => {
31563
31609
  // It's assumed that p1 is the root and p2 is the first peak
31564
31610
  const p1 = coords[0];
31565
31611
  const p2 = coords[1];
@@ -31777,7 +31823,7 @@ function Protractor() {
31777
31823
  const [[xMin, xMax], [yMin, yMax]] = range;
31778
31824
  // Position the protractor in the center of the graph (horizontally), and
31779
31825
  // 95% of the way to the bottom of the graph (vertically).
31780
- const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.05)];
31826
+ const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.25)];
31781
31827
  const [center, setCenter] = useState(initialCenter);
31782
31828
  const [rotationHandleOffset, setRotationHandleOffset] = useState(centerToRotationHandle);
31783
31829
  const draggableRef = useRef(null);
@@ -31939,6 +31985,9 @@ const MafsGraph = props => {
31939
31985
  x: viewboxX,
31940
31986
  y: viewboxY
31941
31987
  };
31988
+ const {
31989
+ strings
31990
+ } = usePerseusI18n();
31942
31991
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
31943
31992
  value: {
31944
31993
  range: state.range,
@@ -32017,6 +32066,7 @@ const MafsGraph = props => {
32017
32066
  })),
32018
32067
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
32019
32068
  props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
32069
+ flags: props.flags,
32020
32070
  lockedFigures: props.lockedFigures,
32021
32071
  range: state.range
32022
32072
  }))), ((_props$flags = props.flags) == null || (_props$flags = _props$flags["mafs"]) == null ? void 0 : _props$flags["interactive-graph-locked-features-labels"]) && props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLabelsLayer, {
@@ -32053,10 +32103,11 @@ const MafsGraph = props => {
32053
32103
  top: "50%",
32054
32104
  transform: "translateY(-50%)"
32055
32105
  }
32056
- }, /*#__PURE__*/React.createElement(LabelMedium, null, "Press ", /*#__PURE__*/React.createElement("strong", null, "Shift + Enter"), " to interact with the graph"))), renderGraphControls({
32106
+ }, /*#__PURE__*/React.createElement(LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
32057
32107
  state,
32058
32108
  dispatch,
32059
- width
32109
+ width,
32110
+ strings
32060
32111
  })));
32061
32112
  };
32062
32113
  const renderPointGraphControls = props => {
@@ -32065,6 +32116,10 @@ const renderPointGraphControls = props => {
32065
32116
  showRemovePointButton,
32066
32117
  focusedPointIndex
32067
32118
  } = props.state;
32119
+ const {
32120
+ strings
32121
+ } = props;
32122
+ const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
32068
32123
  return /*#__PURE__*/React.createElement(View, {
32069
32124
  style: {
32070
32125
  flexDirection: "row",
@@ -32080,7 +32135,7 @@ const renderPointGraphControls = props => {
32080
32135
  onClick: () => {
32081
32136
  props.dispatch(actions.pointGraph.addPoint([0, 0]));
32082
32137
  }
32083
- }, "Add Point"), interactionMode === "mouse" && showRemovePointButton && focusedPointIndex !== null && /*#__PURE__*/React.createElement(Button, {
32138
+ }, strings.addPoint), interactionMode === "mouse" && /*#__PURE__*/React.createElement(Button, {
32084
32139
  id: REMOVE_BUTTON_ID,
32085
32140
  kind: "secondary",
32086
32141
  color: "destructive"
@@ -32090,18 +32145,20 @@ const renderPointGraphControls = props => {
32090
32145
  tabIndex: -1,
32091
32146
  style: {
32092
32147
  width: "100%",
32093
- marginLeft: "20px"
32148
+ marginLeft: "20px",
32149
+ visibility: shouldShowRemoveButton ? "visible" : "hidden"
32094
32150
  },
32095
32151
  onClick: event => {
32096
32152
  props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32097
32153
  }
32098
- }, "Remove Point"));
32154
+ }, strings.removePoint));
32099
32155
  };
32100
32156
  const renderGraphControls = props => {
32101
32157
  const {
32102
32158
  state,
32103
32159
  dispatch,
32104
- width
32160
+ width,
32161
+ strings
32105
32162
  } = props;
32106
32163
  const {
32107
32164
  type
@@ -32112,7 +32169,8 @@ const renderGraphControls = props => {
32112
32169
  return renderPointGraphControls({
32113
32170
  state,
32114
32171
  dispatch,
32115
- width
32172
+ width,
32173
+ strings
32116
32174
  });
32117
32175
  }
32118
32176
  return null;
@@ -32264,6 +32322,83 @@ const renderGraph = props => {
32264
32322
  }
32265
32323
  };
32266
32324
 
32325
+ // Converts the state of a StatefulMafsGraph back to the format used to
32326
+ // represent graph state in the widget JSON.
32327
+ //
32328
+ // Rather than be tightly bound to how data was structured in
32329
+ // the legacy interactive graph, this lets us store state
32330
+ // however we want and we just transform it before handing it off
32331
+ // the the parent InteractiveGraph.
32332
+ //
32333
+ // The transformed data is used in the interactive graph widget editor, to
32334
+ // set the `correct` field of the graph options. In the learner-facing UI, the
32335
+ // data is also stored by the Renderer, and passed back down to the graph
32336
+ // widget via the `graph` prop. Because the data returned by this function
32337
+ // completely replaces the Renderer's representation of the widget, rather than
32338
+ // being merged into it, we take it upon ourselves to merge in the original
32339
+ // widget data here. If we didn't do this merging, the graph's configuration
32340
+ // would reset to the defaults when the learner interacted with it.
32341
+ function mafsStateToInteractiveGraph(state, originalGraph) {
32342
+ switch (state.type) {
32343
+ case "angle":
32344
+ invariant(originalGraph.type === "angle");
32345
+ return _extends({}, originalGraph, {
32346
+ coords: state.coords
32347
+ });
32348
+ case "quadratic":
32349
+ invariant(originalGraph.type === "quadratic");
32350
+ return _extends({}, originalGraph, {
32351
+ coords: state.coords
32352
+ });
32353
+ case "circle":
32354
+ invariant(originalGraph.type === "circle");
32355
+ return _extends({}, originalGraph, {
32356
+ center: state.center,
32357
+ radius: getRadius(state)
32358
+ });
32359
+ case "linear":
32360
+ invariant(originalGraph.type === "linear");
32361
+ return _extends({}, originalGraph, {
32362
+ coords: state.coords
32363
+ });
32364
+ case "ray":
32365
+ invariant(originalGraph.type === "ray");
32366
+ return _extends({}, originalGraph, {
32367
+ coords: state.coords
32368
+ });
32369
+ case "sinusoid":
32370
+ invariant(originalGraph.type === "sinusoid");
32371
+ return _extends({}, originalGraph, {
32372
+ coords: state.coords
32373
+ });
32374
+ case "segment":
32375
+ invariant(originalGraph.type === "segment");
32376
+ return _extends({}, originalGraph, {
32377
+ coords: state.coords
32378
+ });
32379
+ case "linear-system":
32380
+ invariant(originalGraph.type === "linear-system");
32381
+ return _extends({}, originalGraph, {
32382
+ coords: state.coords
32383
+ });
32384
+ case "polygon":
32385
+ invariant(originalGraph.type === "polygon");
32386
+ return _extends({}, originalGraph, {
32387
+ coords: state.coords
32388
+ });
32389
+ case "point":
32390
+ invariant(originalGraph.type === "point");
32391
+ return _extends({}, originalGraph, {
32392
+ coords: state.coords
32393
+ });
32394
+ case "none":
32395
+ invariant(originalGraph.type === "none");
32396
+ return _extends({}, originalGraph);
32397
+ default:
32398
+ throw new UnreachableCaseError(state);
32399
+ }
32400
+ }
32401
+
32267
32402
  function initializeGraphState(params) {
32268
32403
  var _graph$snapTo;
32269
32404
  const {
@@ -32889,7 +33024,7 @@ function doMovePoint(state, action) {
32889
33024
  // Bind the new destination to the graph range/snapStep and then get the quadratic coefficients
32890
33025
  const boundDestination = boundAndSnapToGrid(action.destination, state);
32891
33026
  _newCoords2[action.index] = boundDestination;
32892
- const QuadraticCoefficients = getQuadraticCoefficients(_newCoords2);
33027
+ const QuadraticCoefficients = getQuadraticCoefficients$1(_newCoords2);
32893
33028
 
32894
33029
  // If the new destination results in an invalid quadratic equation, we don't want to move the point
32895
33030
  if (QuadraticCoefficients === undefined) {
@@ -33036,11 +33171,11 @@ const getChange = (coords, delta, constraintOpts) => {
33036
33171
  const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
33037
33172
  return [dx, dy];
33038
33173
  };
33039
- const eq$1 = Util.eq;
33174
+ const eq$2 = Util.eq;
33040
33175
 
33041
33176
  // Less than or approximately equal
33042
33177
  function leq$1(a, b) {
33043
- return a < b || eq$1(a, b);
33178
+ return a < b || eq$2(a, b);
33044
33179
  }
33045
33180
  function boundAndSnapToGrid(point, {
33046
33181
  snapStep,
@@ -33343,20 +33478,6 @@ function setAtIndex(args) {
33343
33478
  return copy;
33344
33479
  }
33345
33480
 
33346
- // Rather than be tightly bound to how data was structured in
33347
- // the legacy interactive graph, this lets us store state
33348
- // however we want and we just transform it before handing it off
33349
- // the the parent InteractiveGraph
33350
- function mafsStateToInteractiveGraph(state) {
33351
- if (state.graph.type === "circle") {
33352
- return _extends({}, state, {
33353
- graph: _extends({}, state.graph, {
33354
- radius: getRadius(state.graph)
33355
- })
33356
- });
33357
- }
33358
- return _extends({}, state);
33359
- }
33360
33481
  const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33361
33482
  const {
33362
33483
  onChange,
@@ -33369,12 +33490,12 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33369
33490
  const prevState = useRef(state);
33370
33491
  useEffect(() => {
33371
33492
  if (prevState.current !== state) {
33372
- onChange(mafsStateToInteractiveGraph({
33373
- graph: state
33374
- }));
33493
+ onChange({
33494
+ graph: mafsStateToInteractiveGraph(state, graph)
33495
+ });
33375
33496
  }
33376
33497
  prevState.current = state;
33377
- }, [onChange, state]);
33498
+ }, [onChange, state, graph]);
33378
33499
 
33379
33500
  // Destructuring first to keep useEffect from making excess calls
33380
33501
  const [xSnap, ySnap] = props.snapStep;
@@ -33426,12 +33547,234 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33426
33547
  }));
33427
33548
  });
33428
33549
 
33550
+ const eq$1 = Util.eq;
33551
+ const deepEq$1 = Util.deepEq;
33552
+ function interactiveGraphValidator(userInput, rubric) {
33553
+ // None-type graphs are not graded
33554
+ if (userInput.type === "none" && rubric.correct.type === "none") {
33555
+ return {
33556
+ type: "points",
33557
+ earned: 0,
33558
+ total: 0,
33559
+ message: null
33560
+ };
33561
+ }
33562
+
33563
+ // When nothing has moved, there will neither be coords nor the
33564
+ // circle's center/radius fields. When those fields are absent, skip
33565
+ // all these checks; just go mark the answer as empty.
33566
+ const hasValue = Boolean(
33567
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
33568
+ userInput.coords ||
33569
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
33570
+ userInput.center && userInput.radius);
33571
+ if (userInput.type === rubric.correct.type && hasValue) {
33572
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
33573
+ const guess = userInput.coords;
33574
+ const correct = rubric.correct.coords;
33575
+
33576
+ // If both of the guess points are on the correct line, it's
33577
+ // correct.
33578
+ if (
33579
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33580
+ collinear(correct[0], correct[1], guess[0]) &&
33581
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33582
+ collinear(correct[0], correct[1], guess[1])) {
33583
+ return {
33584
+ type: "points",
33585
+ earned: 1,
33586
+ total: 1,
33587
+ message: null
33588
+ };
33589
+ }
33590
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
33591
+ const guess = userInput.coords;
33592
+ const correct = rubric.correct.coords;
33593
+ if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
33594
+ return {
33595
+ type: "points",
33596
+ earned: 1,
33597
+ total: 1,
33598
+ message: null
33599
+ };
33600
+ }
33601
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
33602
+ // If the parabola coefficients match, it's correct.
33603
+ const guessCoeffs = getQuadraticCoefficients(userInput.coords);
33604
+ const correctCoeffs = getQuadraticCoefficients(
33605
+ // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
33606
+ rubric.correct.coords);
33607
+ if (deepEq$1(guessCoeffs, correctCoeffs)) {
33608
+ return {
33609
+ type: "points",
33610
+ earned: 1,
33611
+ total: 1,
33612
+ message: null
33613
+ };
33614
+ }
33615
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
33616
+ const guessCoeffs = getSinusoidCoefficients(userInput.coords);
33617
+ const correctCoeffs = getSinusoidCoefficients(
33618
+ // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
33619
+ rubric.correct.coords);
33620
+ const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
33621
+ const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
33622
+ // If the canonical coefficients match, it's correct.
33623
+ if (deepEq$1(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
33624
+ return {
33625
+ type: "points",
33626
+ earned: 1,
33627
+ total: 1,
33628
+ message: null
33629
+ };
33630
+ }
33631
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
33632
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
33633
+ return {
33634
+ type: "points",
33635
+ earned: 1,
33636
+ total: 1,
33637
+ message: null
33638
+ };
33639
+ }
33640
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
33641
+ let correct = rubric.correct.coords;
33642
+ if (correct == null) {
33643
+ throw new Error("Point graph rubric has null coords");
33644
+ }
33645
+ const guess = userInput.coords.slice();
33646
+ correct = correct.slice();
33647
+ // Everything's already rounded so we shouldn't need to do an
33648
+ // eq() comparison but _.isEqual(0, -0) is false, so we'll use
33649
+ // eq() anyway. The sort should be fine because it'll stringify
33650
+ // it and -0 converted to a string is "0"
33651
+ guess == null || guess.sort();
33652
+ // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
33653
+ correct.sort();
33654
+ if (deepEq$1(guess, correct)) {
33655
+ return {
33656
+ type: "points",
33657
+ earned: 1,
33658
+ total: 1,
33659
+ message: null
33660
+ };
33661
+ }
33662
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
33663
+ var _userInput$coords, _rubric$correct$coord;
33664
+ const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
33665
+ // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
33666
+ const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
33667
+ let match;
33668
+ if (rubric.correct.match === "similar") {
33669
+ match = similar(guess, correct, Number.POSITIVE_INFINITY);
33670
+ } else if (rubric.correct.match === "congruent") {
33671
+ match = similar(guess, correct, number.DEFAULT_TOLERANCE);
33672
+ } else if (rubric.correct.match === "approx") {
33673
+ match = similar(guess, correct, 0.1);
33674
+ } else {
33675
+ /* exact */
33676
+ guess.sort();
33677
+ correct.sort();
33678
+ match = deepEq$1(guess, correct);
33679
+ }
33680
+ if (match) {
33681
+ return {
33682
+ type: "points",
33683
+ earned: 1,
33684
+ total: 1,
33685
+ message: null
33686
+ };
33687
+ }
33688
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
33689
+ var _rubric$correct;
33690
+ let guess = Util.deepClone(userInput.coords);
33691
+ let correct = Util.deepClone((_rubric$correct = rubric.correct) == null ? void 0 : _rubric$correct.coords);
33692
+ guess = _.invoke(guess, "sort").sort();
33693
+ // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
33694
+ correct = _.invoke(correct, "sort").sort();
33695
+ if (deepEq$1(guess, correct)) {
33696
+ return {
33697
+ type: "points",
33698
+ earned: 1,
33699
+ total: 1,
33700
+ message: null
33701
+ };
33702
+ }
33703
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
33704
+ const guess = userInput.coords;
33705
+ const correct = rubric.correct.coords;
33706
+ if (
33707
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33708
+ deepEq$1(guess[0], correct[0]) &&
33709
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33710
+ collinear(correct[0], correct[1], guess[1])) {
33711
+ return {
33712
+ type: "points",
33713
+ earned: 1,
33714
+ total: 1,
33715
+ message: null
33716
+ };
33717
+ }
33718
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
33719
+ const guess = userInput.coords;
33720
+ const correct = rubric.correct.coords;
33721
+ let match;
33722
+ if (rubric.correct.match === "congruent") {
33723
+ const angles = _.map([guess, correct], function (coords) {
33724
+ const angle = GraphUtils.findAngle(
33725
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33726
+ coords[2],
33727
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33728
+ coords[0],
33729
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
33730
+ coords[1]);
33731
+ return (angle + 360) % 360;
33732
+ });
33733
+ // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
33734
+ match = eq$1(...angles);
33735
+ } else {
33736
+ /* exact */
33737
+ match =
33738
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33739
+ deepEq$1(guess[1], correct[1]) &&
33740
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33741
+ collinear(correct[1], correct[0], guess[0]) &&
33742
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33743
+ collinear(correct[1], correct[2], guess[2]);
33744
+ }
33745
+ if (match) {
33746
+ return {
33747
+ type: "points",
33748
+ earned: 1,
33749
+ total: 1,
33750
+ message: null
33751
+ };
33752
+ }
33753
+ }
33754
+ }
33755
+
33756
+ // The input wasn't correct, so check if it's a blank input or if it's
33757
+ // actually just wrong
33758
+ if (!hasValue || _.isEqual(userInput, rubric.graph)) {
33759
+ // We're where we started.
33760
+ return {
33761
+ type: "invalid",
33762
+ message: null
33763
+ };
33764
+ }
33765
+ return {
33766
+ type: "points",
33767
+ earned: 0,
33768
+ total: 1,
33769
+ message: null
33770
+ };
33771
+ }
33772
+
33429
33773
  const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
33430
33774
  const defaultBackgroundImage = {
33431
33775
  url: null
33432
33776
  };
33433
33777
  const eq = Util.eq;
33434
- const deepEq$1 = Util.deepEq;
33435
33778
  const UNLIMITED = "unlimited";
33436
33779
 
33437
33780
  // Sample background image:
@@ -33472,6 +33815,39 @@ const makeInvalidTypeError = (functionName, graphType) => {
33472
33815
 
33473
33816
  // There's no transform function in exports
33474
33817
 
33818
+ // TODO: there's another, very similar getSinusoidCoefficients function
33819
+ // they should probably be merged
33820
+ function getSinusoidCoefficients(coords) {
33821
+ // It's assumed that p1 is the root and p2 is the first peak
33822
+ const p1 = coords[0];
33823
+ const p2 = coords[1];
33824
+
33825
+ // Resulting coefficients are canonical for this sine curve
33826
+ const amplitude = p2[1] - p1[1];
33827
+ const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
33828
+ const phase = p1[0] * angularFrequency;
33829
+ const verticalOffset = p1[1];
33830
+ return [amplitude, angularFrequency, phase, verticalOffset];
33831
+ }
33832
+
33833
+ // TODO: there's another, very similar getQuadraticCoefficients function
33834
+ // they should probably be merged
33835
+ function getQuadraticCoefficients(coords) {
33836
+ const p1 = coords[0];
33837
+ const p2 = coords[1];
33838
+ const p3 = coords[2];
33839
+ const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
33840
+ if (denom === 0) {
33841
+ // Many of the callers assume that the return value is always defined.
33842
+ // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
33843
+ return;
33844
+ }
33845
+ const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
33846
+ const b = (p3[0] * p3[0] * (p1[1] - p2[1]) + p2[0] * p2[0] * (p3[1] - p1[1]) + p1[0] * p1[0] * (p2[1] - p3[1])) / denom;
33847
+ const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
33848
+ return [a, b, c];
33849
+ }
33850
+
33475
33851
  // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
33476
33852
  // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
33477
33853
  // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
@@ -34555,7 +34931,6 @@ class LegacyInteractiveGraph extends React.Component {
34555
34931
  $(this.angle).on("move", () => {
34556
34932
  var _this$angle;
34557
34933
  this.onChange({
34558
- // @ts-expect-error Type '{ coords: any; type: "angle"; showAngles?: boolean | undefined; allowReflexAngles?: boolean | undefined; angleOffsetDeg?: number | undefined; snapDegrees?: number | undefined; match?: "congruent" | undefined; }' is not assignable to type 'InteractiveGraphState | undefined'.
34559
34934
  graph: _extends({}, graph, {
34560
34935
  coords: (_this$angle = this.angle) == null ? void 0 : _this$angle.getClockwiseCoords()
34561
34936
  })
@@ -34629,7 +35004,7 @@ class LegacyInteractiveGraph extends React.Component {
34629
35004
  return InteractiveGraph.getUserInputFromProps(this.props);
34630
35005
  }
34631
35006
  simpleValidate(rubric) {
34632
- return InteractiveGraph.validate(this.getUserInput(), rubric, this);
35007
+ return interactiveGraphValidator(this.getUserInput(), rubric);
34633
35008
  }
34634
35009
  render() {
34635
35010
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -34696,20 +35071,19 @@ class InteractiveGraph extends React.Component {
34696
35071
  super(...args);
34697
35072
  this.legacyGraphRef = /*#__PURE__*/React.createRef();
34698
35073
  this.mafsRef = /*#__PURE__*/React.createRef();
34699
- this.getUserInput = () => {
34700
- if (this.mafsRef.current) {
34701
- var _this$mafsRef$current, _this$mafsRef$current2;
34702
- return (_this$mafsRef$current = (_this$mafsRef$current2 = this.mafsRef.current).getUserInput) == null ? void 0 : _this$mafsRef$current.call(_this$mafsRef$current2);
34703
- }
34704
- if (this.legacyGraphRef.current) {
34705
- return this.legacyGraphRef.current.getUserInput();
34706
- }
34707
- throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
34708
- };
35074
+ }
35075
+ getUserInput() {
35076
+ var _this$mafsRef$current, _this$legacyGraphRef$;
35077
+ if ((_this$mafsRef$current = this.mafsRef.current) != null && _this$mafsRef$current.getUserInput) {
35078
+ return this.mafsRef.current.getUserInput();
35079
+ }
35080
+ if ((_this$legacyGraphRef$ = this.legacyGraphRef.current) != null && _this$legacyGraphRef$.getUserInput) {
35081
+ return this.legacyGraphRef.current.getUserInput();
35082
+ }
35083
+ throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
34709
35084
  }
34710
35085
  simpleValidate(rubric) {
34711
- var _this$getUserInput;
34712
- return InteractiveGraph.validate((_this$getUserInput = this.getUserInput) == null ? void 0 : _this$getUserInput.call(this), rubric, this);
35086
+ return interactiveGraphValidator(this.getUserInput(), rubric);
34713
35087
  }
34714
35088
  render() {
34715
35089
  var _this$props$apiOption;
@@ -34734,33 +35108,6 @@ class InteractiveGraph extends React.Component {
34734
35108
  ref: this.legacyGraphRef
34735
35109
  }, this.props));
34736
35110
  }
34737
- static getQuadraticCoefficients(coords) {
34738
- const p1 = coords[0];
34739
- const p2 = coords[1];
34740
- const p3 = coords[2];
34741
- const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
34742
- if (denom === 0) {
34743
- // Many of the callers assume that the return value is always defined.
34744
- // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
34745
- return;
34746
- }
34747
- const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
34748
- const b = (p3[0] * p3[0] * (p1[1] - p2[1]) + p2[0] * p2[0] * (p3[1] - p1[1]) + p1[0] * p1[0] * (p2[1] - p3[1])) / denom;
34749
- const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
34750
- return [a, b, c];
34751
- }
34752
- static getSinusoidCoefficients(coords) {
34753
- // It's assumed that p1 is the root and p2 is the first peak
34754
- const p1 = coords[0];
34755
- const p2 = coords[1];
34756
-
34757
- // Resulting coefficients are canonical for this sine curve
34758
- const amplitude = p2[1] - p1[1];
34759
- const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
34760
- const phase = p1[0] * angularFrequency;
34761
- const verticalOffset = p1[1];
34762
- return [amplitude, angularFrequency, phase, verticalOffset];
34763
- }
34764
35111
 
34765
35112
  /**
34766
35113
  * @param {object} graph Like props.graph or props.correct
@@ -34974,7 +35321,7 @@ class InteractiveGraph extends React.Component {
34974
35321
  const coords =
34975
35322
  // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
34976
35323
  props.graph.coords || InteractiveGraph.defaultQuadraticCoords(props);
34977
- return InteractiveGraph.getQuadraticCoefficients(coords);
35324
+ return getQuadraticCoefficients(coords);
34978
35325
  }
34979
35326
  static defaultQuadraticCoords(props) {
34980
35327
  const coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
@@ -34989,7 +35336,7 @@ class InteractiveGraph extends React.Component {
34989
35336
  const coords =
34990
35337
  // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
34991
35338
  props.graph.coords || InteractiveGraph.defaultSinusoidCoords(props);
34992
- return InteractiveGraph.getSinusoidCoefficients(coords);
35339
+ return getSinusoidCoefficients(coords);
34993
35340
  }
34994
35341
  static defaultSinusoidCoords(props) {
34995
35342
  const coords = [[0.5, 0.5], [0.65, 0.6]];
@@ -35069,222 +35416,8 @@ class InteractiveGraph extends React.Component {
35069
35416
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
35070
35417
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
35071
35418
  }
35072
- static validate(userInput, rubric, component) {
35073
- // None-type graphs are not graded
35074
- if (userInput.type === "none" && rubric.correct.type === "none") {
35075
- return {
35076
- type: "points",
35077
- earned: 0,
35078
- total: 0,
35079
- message: null
35080
- };
35081
- }
35082
-
35083
- // When nothing has moved, there will neither be coords nor the
35084
- // circle's center/radius fields. When those fields are absent, skip
35085
- // all these checks; just go mark the answer as empty.
35086
- const hasValue = Boolean(
35087
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35088
- userInput.coords ||
35089
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
35090
- userInput.center && userInput.radius);
35091
- if (userInput.type === rubric.correct.type && hasValue) {
35092
- if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
35093
- const guess = userInput.coords;
35094
- const correct = rubric.correct.coords;
35095
-
35096
- // If both of the guess points are on the correct line, it's
35097
- // correct.
35098
- if (
35099
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35100
- collinear(correct[0], correct[1], guess[0]) &&
35101
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35102
- collinear(correct[0], correct[1], guess[1])) {
35103
- return {
35104
- type: "points",
35105
- earned: 1,
35106
- total: 1,
35107
- message: null
35108
- };
35109
- }
35110
- } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
35111
- const guess = userInput.coords;
35112
- const correct = rubric.correct.coords;
35113
- if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
35114
- return {
35115
- type: "points",
35116
- earned: 1,
35117
- total: 1,
35118
- message: null
35119
- };
35120
- }
35121
- } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
35122
- // If the parabola coefficients match, it's correct.
35123
- const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
35124
- const correctCoeffs = this.getQuadraticCoefficients(
35125
- // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
35126
- rubric.correct.coords);
35127
- if (deepEq$1(guessCoeffs, correctCoeffs)) {
35128
- return {
35129
- type: "points",
35130
- earned: 1,
35131
- total: 1,
35132
- message: null
35133
- };
35134
- }
35135
- } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
35136
- const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
35137
- const correctCoeffs = this.getSinusoidCoefficients(
35138
- // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
35139
- rubric.correct.coords);
35140
- const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
35141
- const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
35142
- // If the canonical coefficients match, it's correct.
35143
- if (deepEq$1(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
35144
- return {
35145
- type: "points",
35146
- earned: 1,
35147
- total: 1,
35148
- message: null
35149
- };
35150
- }
35151
- } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
35152
- if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
35153
- return {
35154
- type: "points",
35155
- earned: 1,
35156
- total: 1,
35157
- message: null
35158
- };
35159
- }
35160
- } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
35161
- let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
35162
- const guess = userInput.coords.slice();
35163
- correct = correct.slice();
35164
- // Everything's already rounded so we shouldn't need to do an
35165
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
35166
- // eq() anyway. The sort should be fine because it'll stringify
35167
- // it and -0 converted to a string is "0"
35168
- guess == null || guess.sort();
35169
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
35170
- correct.sort();
35171
- if (deepEq$1(guess, correct)) {
35172
- return {
35173
- type: "points",
35174
- earned: 1,
35175
- total: 1,
35176
- message: null
35177
- };
35178
- }
35179
- } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
35180
- var _userInput$coords, _rubric$correct$coord;
35181
- const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
35182
- // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
35183
- const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
35184
- let match;
35185
- if (rubric.correct.match === "similar") {
35186
- match = similar(guess, correct, Number.POSITIVE_INFINITY);
35187
- } else if (rubric.correct.match === "congruent") {
35188
- match = similar(guess, correct, number.DEFAULT_TOLERANCE);
35189
- } else if (rubric.correct.match === "approx") {
35190
- match = similar(guess, correct, 0.1);
35191
- } else {
35192
- /* exact */
35193
- guess.sort();
35194
- correct.sort();
35195
- match = deepEq$1(guess, correct);
35196
- }
35197
- if (match) {
35198
- return {
35199
- type: "points",
35200
- earned: 1,
35201
- total: 1,
35202
- message: null
35203
- };
35204
- }
35205
- } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
35206
- var _rubric$correct;
35207
- let guess = Util.deepClone(userInput.coords);
35208
- let correct = Util.deepClone((_rubric$correct = rubric.correct) == null ? void 0 : _rubric$correct.coords);
35209
- guess = _.invoke(guess, "sort").sort();
35210
- // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
35211
- correct = _.invoke(correct, "sort").sort();
35212
- if (deepEq$1(guess, correct)) {
35213
- return {
35214
- type: "points",
35215
- earned: 1,
35216
- total: 1,
35217
- message: null
35218
- };
35219
- }
35220
- } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
35221
- const guess = userInput.coords;
35222
- const correct = rubric.correct.coords;
35223
- if (
35224
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35225
- deepEq$1(guess[0], correct[0]) &&
35226
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35227
- collinear(correct[0], correct[1], guess[1])) {
35228
- return {
35229
- type: "points",
35230
- earned: 1,
35231
- total: 1,
35232
- message: null
35233
- };
35234
- }
35235
- } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
35236
- const guess = userInput.coords;
35237
- const correct = rubric.correct.coords;
35238
- let match;
35239
- if (rubric.correct.match === "congruent") {
35240
- const angles = _.map([guess, correct], function (coords) {
35241
- const angle = GraphUtils.findAngle(
35242
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35243
- coords[2],
35244
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35245
- coords[0],
35246
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35247
- coords[1]);
35248
- return (angle + 360) % 360;
35249
- });
35250
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
35251
- match = eq(...angles);
35252
- } else {
35253
- /* exact */
35254
- match =
35255
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35256
- deepEq$1(guess[1], correct[1]) &&
35257
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35258
- collinear(correct[1], correct[0], guess[0]) &&
35259
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35260
- collinear(correct[1], correct[2], guess[2]);
35261
- }
35262
- if (match) {
35263
- return {
35264
- type: "points",
35265
- earned: 1,
35266
- total: 1,
35267
- message: null
35268
- };
35269
- }
35270
- }
35271
- }
35272
-
35273
- // The input wasn't correct, so check if it's a blank input or if it's
35274
- // actually just wrong
35275
- if (!hasValue || _.isEqual(userInput, rubric.graph)) {
35276
- // We're where we started.
35277
- return {
35278
- type: "invalid",
35279
- message: null
35280
- };
35281
- }
35282
- return {
35283
- type: "points",
35284
- earned: 0,
35285
- total: 1,
35286
- message: null
35287
- };
35419
+ static validate(userInput, rubric) {
35420
+ return interactiveGraphValidator(userInput, rubric);
35288
35421
  }
35289
35422
  static getUserInputFromProps(props) {
35290
35423
  return props.graph;
@@ -36387,6 +36520,57 @@ const styles$h = StyleSheet.create({
36387
36520
  }
36388
36521
  });
36389
36522
 
36523
+ // Question state for marker as result of user selected answers.
36524
+
36525
+ function scoreMarker(marker) {
36526
+ const score = {
36527
+ hasAnswers: false,
36528
+ isCorrect: false
36529
+ };
36530
+ if (marker.selected && marker.selected.length > 0) {
36531
+ score.hasAnswers = true;
36532
+ }
36533
+ if (marker.answers.length > 0) {
36534
+ if (marker.selected && marker.selected.length === marker.answers.length) {
36535
+ // All correct answers are selected by the user.
36536
+ score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36537
+ }
36538
+ } else if (!marker.selected || marker.selected.length === 0) {
36539
+ // Correct as no answers should be selected by the user.
36540
+ score.isCorrect = true;
36541
+ }
36542
+ return score;
36543
+ }
36544
+ function labelImageValidator(state, rubric) {
36545
+ let numAnswered = 0;
36546
+ let numCorrect = 0;
36547
+ for (const marker of state.markers) {
36548
+ const score = scoreMarker(marker);
36549
+ if (score.hasAnswers) {
36550
+ numAnswered++;
36551
+ }
36552
+ if (score.isCorrect) {
36553
+ numCorrect++;
36554
+ }
36555
+ }
36556
+
36557
+ // We expect all question markers to be answered before grading.
36558
+ if (numAnswered !== state.markers.length) {
36559
+ return {
36560
+ type: "invalid",
36561
+ message: null
36562
+ };
36563
+ }
36564
+ return {
36565
+ type: "points",
36566
+ // Markers with no expected answers are graded as correct if user
36567
+ // makes no answer selection.
36568
+ earned: numCorrect === state.markers.length ? 1 : 0,
36569
+ total: 1,
36570
+ message: null
36571
+ };
36572
+ }
36573
+
36390
36574
  const BringToFront = {
36391
36575
  boxShadow: `0 8px 8px ${color.offBlack64}`,
36392
36576
  zIndex: 1000
@@ -36645,57 +36829,11 @@ const styles$f = StyleSheet.create({
36645
36829
  * No diagonals allowed.
36646
36830
  */
36647
36831
 
36832
+ // TODO: should this be using WidgetProps / PerseusLabelImageWidgetOptions?
36833
+
36648
36834
  class LabelImage extends React.Component {
36649
- static gradeMarker(marker) {
36650
- const score = {
36651
- hasAnswers: false,
36652
- isCorrect: false
36653
- };
36654
- if (marker.selected && marker.selected.length > 0) {
36655
- // @ts-expect-error - TS2540 - Cannot assign to 'hasAnswers' because it is a read-only property.
36656
- score.hasAnswers = true;
36657
- }
36658
- if (marker.answers.length > 0) {
36659
- if (marker.selected && marker.selected.length === marker.answers.length) {
36660
- // All correct answers are selected by the user.
36661
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
36662
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36663
- }
36664
- } else if (!marker.selected || marker.selected.length === 0) {
36665
- // Correct as no answers should be selected by the user.
36666
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
36667
- score.isCorrect = true;
36668
- }
36669
- return score;
36670
- }
36671
36835
  static validate(state, rubric) {
36672
- let numAnswered = 0;
36673
- let numCorrect = 0;
36674
- for (const marker of state.markers) {
36675
- const score = LabelImage.gradeMarker(marker);
36676
- if (score.hasAnswers) {
36677
- numAnswered++;
36678
- }
36679
- if (score.isCorrect) {
36680
- numCorrect++;
36681
- }
36682
- }
36683
-
36684
- // We expect all question markers to be answered before grading.
36685
- if (numAnswered !== state.markers.length) {
36686
- return {
36687
- type: "invalid",
36688
- message: null
36689
- };
36690
- }
36691
- return {
36692
- type: "points",
36693
- // Markers with no expected answers are graded as correct if user
36694
- // makes no answer selection.
36695
- earned: numCorrect === state.markers.length ? 1 : 0,
36696
- total: 1,
36697
- message: null
36698
- };
36836
+ return labelImageValidator(state);
36699
36837
  }
36700
36838
 
36701
36839
  /**
@@ -36893,7 +37031,7 @@ class LabelImage extends React.Component {
36893
37031
  this._mounted = false;
36894
37032
  }
36895
37033
  simpleValidate(rubric) {
36896
- return LabelImage.validate(this.getUserInput(), rubric);
37034
+ return labelImageValidator(this.getUserInput());
36897
37035
  }
36898
37036
  getUserInput() {
36899
37037
  const {
@@ -36911,7 +37049,7 @@ class LabelImage extends React.Component {
36911
37049
  onChange
36912
37050
  } = this.props;
36913
37051
  const updatedMarkers = markers.map(marker => {
36914
- const score = LabelImage.gradeMarker(marker);
37052
+ const score = scoreMarker(marker);
36915
37053
  return _extends({}, marker, {
36916
37054
  // Reveal correctness state for markers with answers.
36917
37055
  showCorrectness: score.hasAnswers ? score.isCorrect ? "correct" : "incorrect" : undefined
@@ -37058,7 +37196,7 @@ class LabelImage extends React.Component {
37058
37196
  bottom: "top"
37059
37197
  }[markerPosition];
37060
37198
  }
37061
- const score = LabelImage.gradeMarker(marker);
37199
+ const score = scoreMarker(marker);
37062
37200
  // Once the question is answered, show markers
37063
37201
  // with correct answers, otherwise passthrough
37064
37202
  // the correctness state.
@@ -37257,9 +37395,7 @@ const LabelImageWithDependencies = /*#__PURE__*/React.forwardRef((props, ref) =>
37257
37395
  // methods and instead adjust Peresus to provide these facilities through
37258
37396
  // instance methods on our Renderers.
37259
37397
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
37260
- LabelImageWithDependencies.validate = LabelImage.validate;
37261
- // @ts-expect-error - TS2339 - Property 'gradeMarker' does not exist on type
37262
- LabelImageWithDependencies.gradeMarker = LabelImage.gradeMarker;
37398
+ LabelImageWithDependencies.validate = labelImageValidator;
37263
37399
  var LabelImage$1 = {
37264
37400
  name: "label-image",
37265
37401
  displayName: "Label Image",
@@ -37977,6 +38113,16 @@ const styles$d = StyleSheet.create({
37977
38113
  }
37978
38114
  });
37979
38115
 
38116
+ function matcherValidator(state, rubric) {
38117
+ const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
38118
+ return {
38119
+ type: "points",
38120
+ earned: correct ? 1 : 0,
38121
+ total: 1,
38122
+ message: null
38123
+ };
38124
+ }
38125
+
37980
38126
  const {
37981
38127
  shuffle: shuffle$1,
37982
38128
  seededRNG
@@ -38039,10 +38185,12 @@ class Matcher extends React.Component {
38039
38185
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
38040
38186
  this.refs.right.moveOptionToIndex(option, index);
38041
38187
  };
38042
- this.simpleValidate = rubric => {
38043
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matcher'.
38044
- return Matcher.validate(this.getUserInput(), rubric);
38045
- };
38188
+ }
38189
+ static validate(state, rubric) {
38190
+ return matcherValidator(state, rubric);
38191
+ }
38192
+ simpleValidate(rubric) {
38193
+ return matcherValidator(this.getUserInput(), rubric);
38046
38194
  }
38047
38195
  render() {
38048
38196
  // To minimize layout shift, we display a spinner until our math
@@ -38149,17 +38297,6 @@ Matcher.defaultProps = {
38149
38297
  onChange: function () {},
38150
38298
  linterContext: linterContextDefault
38151
38299
  };
38152
- _.extend(Matcher, {
38153
- validate: function (state, rubric) {
38154
- const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
38155
- return {
38156
- type: "points",
38157
- earned: correct ? 1 : 0,
38158
- total: 1,
38159
- message: null
38160
- };
38161
- }
38162
- });
38163
38300
  const padding = 5;
38164
38301
  const border = "1px solid #444";
38165
38302
  const styles$c = StyleSheet.create({
@@ -38195,7 +38332,59 @@ var Matcher$1 = {
38195
38332
  isLintable: true
38196
38333
  };
38197
38334
 
38198
- // Type imports
38335
+ function matrixValidator(state, rubric, strings) {
38336
+ const solution = rubric.answers;
38337
+ const supplied = state.answers;
38338
+ const solutionSize = getMatrixSize(solution);
38339
+ const suppliedSize = getMatrixSize(supplied);
38340
+ const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
38341
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
38342
+ let message = null;
38343
+ let hasEmptyCell = false;
38344
+ let incorrect = false;
38345
+ _(suppliedSize[0]).times(row => {
38346
+ _(suppliedSize[1]).times(col => {
38347
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
38348
+ hasEmptyCell = true;
38349
+ }
38350
+ if (!incorrectSize) {
38351
+ const validator = createValidator(
38352
+ // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
38353
+ solution[row][col], {
38354
+ simplify: true
38355
+ }, strings);
38356
+ const result = validator(supplied[row][col]);
38357
+ if (result.message) {
38358
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
38359
+ message = result.message;
38360
+ }
38361
+ if (!result.correct) {
38362
+ incorrect = true;
38363
+ }
38364
+ }
38365
+ });
38366
+ });
38367
+ if (hasEmptyCell) {
38368
+ return {
38369
+ type: "invalid",
38370
+ message: strings.fillAllCells
38371
+ };
38372
+ }
38373
+ if (incorrectSize) {
38374
+ return {
38375
+ type: "points",
38376
+ earned: 0,
38377
+ total: 1,
38378
+ message: null
38379
+ };
38380
+ }
38381
+ return {
38382
+ type: "points",
38383
+ earned: incorrect ? 0 : 1,
38384
+ total: 1,
38385
+ message: message
38386
+ };
38387
+ }
38199
38388
 
38200
38389
  const {
38201
38390
  assert: assert$2
@@ -38245,7 +38434,7 @@ const getRefForPath$1 = function getRefForPath(path) {
38245
38434
  const column = getColumnFromPath$1(path);
38246
38435
  return "answer" + row + "," + column;
38247
38436
  };
38248
- const getMatrixSize = function getMatrixSize(matrix) {
38437
+ function getMatrixSize(matrix) {
38249
38438
  const matrixSize = [1, 1];
38250
38439
 
38251
38440
  // We need to find the widest row and tallest column to get the correct
@@ -38267,7 +38456,7 @@ const getMatrixSize = function getMatrixSize(matrix) {
38267
38456
  }
38268
38457
  });
38269
38458
  return matrixSize;
38270
- };
38459
+ }
38271
38460
  class Matrix extends React.Component {
38272
38461
  constructor(...args) {
38273
38462
  super(...args);
@@ -38406,70 +38595,22 @@ class Matrix extends React.Component {
38406
38595
  }, cb);
38407
38596
  this.props.trackInteraction();
38408
38597
  };
38409
- this.getUserInput = () => {
38410
- return {
38411
- answers: this.props.answers
38412
- };
38413
- };
38414
- this.simpleValidate = rubric => {
38415
- return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
38416
- };
38417
38598
  }
38418
38599
  static validate(state, rubric, strings) {
38419
- const solution = rubric.answers;
38420
- const supplied = state.answers;
38421
- const solutionSize = getMatrixSize(solution);
38422
- const suppliedSize = getMatrixSize(supplied);
38423
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
38424
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
38425
- let message = null;
38426
- let hasEmptyCell = false;
38427
- let incorrect = false;
38428
- _(suppliedSize[0]).times(row => {
38429
- _(suppliedSize[1]).times(col => {
38430
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
38431
- hasEmptyCell = true;
38432
- }
38433
- if (!incorrectSize) {
38434
- const validator = createValidator(solution[row][col], {
38435
- simplify: true
38436
- }, strings);
38437
- const result = validator(supplied[row][col]);
38438
- if (result.message) {
38439
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
38440
- message = result.message;
38441
- }
38442
- if (!result.correct) {
38443
- incorrect = true;
38444
- }
38445
- }
38446
- });
38447
- });
38448
- if (hasEmptyCell) {
38449
- return {
38450
- type: "invalid",
38451
- message: strings.fillAllCells
38452
- };
38453
- }
38454
- if (incorrectSize) {
38455
- return {
38456
- type: "points",
38457
- earned: 0,
38458
- total: 1,
38459
- message: null
38460
- };
38461
- }
38462
- return {
38463
- type: "points",
38464
- earned: incorrect ? 0 : 1,
38465
- total: 1,
38466
- message: message
38467
- };
38600
+ return matrixValidator(state, rubric, strings);
38468
38601
  }
38469
38602
  componentDidMount() {
38470
38603
  // Used in the `onBlur` and `onFocus` handlers
38471
38604
  this.cursorPosition = [0, 0];
38472
38605
  }
38606
+ getUserInput() {
38607
+ return {
38608
+ answers: this.props.answers
38609
+ };
38610
+ }
38611
+ simpleValidate(rubric) {
38612
+ return matrixValidator(this.getUserInput(), rubric, this.context.strings);
38613
+ }
38473
38614
  render() {
38474
38615
  // Set the input sizes through JS so we can control the size of the
38475
38616
  // brackets. (If we set them in CSS we won't know values until the
@@ -39680,16 +39821,14 @@ class Molecule extends React.Component {
39680
39821
  }
39681
39822
  Molecule.contextType = PerseusI18nContext;
39682
39823
  class MoleculeWidget extends React.Component {
39683
- constructor(...args) {
39684
- super(...args);
39685
- this.getUserInput = () => {
39686
- return [];
39687
- };
39688
- }
39689
39824
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
39690
39825
  simpleValidate() {
39691
39826
  return noopValidator();
39692
39827
  }
39828
+ getUserInput() {
39829
+ return [];
39830
+ }
39831
+
39693
39832
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
39694
39833
  validate() {
39695
39834
  return noopValidator();
@@ -39712,6 +39851,45 @@ var Molecule$1 = {
39712
39851
  widget: MoleculeWidget
39713
39852
  };
39714
39853
 
39854
+ function numberLineValidator(state, rubric) {
39855
+ const range = rubric.range;
39856
+ const divisionRange = state.divisionRange;
39857
+ const start = rubric.initialX != null ? rubric.initialX : range[0];
39858
+ const startRel = rubric.isInequality ? "ge" : "eq";
39859
+ const correctRel = rubric.correctRel || "eq";
39860
+ const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
39861
+ const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
39862
+
39863
+ // TODO: I don't think isTickCrtl is a thing anymore
39864
+ if (state.isTickCrtl && outsideAllowedRange) {
39865
+ return {
39866
+ type: "invalid",
39867
+ message: "Number of divisions is outside the allowed range."
39868
+ };
39869
+ }
39870
+ if (correctPos && correctRel === state.rel) {
39871
+ return {
39872
+ type: "points",
39873
+ earned: 1,
39874
+ total: 1,
39875
+ message: null
39876
+ };
39877
+ }
39878
+ if (state.numLinePosition === start && state.rel === startRel) {
39879
+ // We're where we started.
39880
+ return {
39881
+ type: "invalid",
39882
+ message: null
39883
+ };
39884
+ }
39885
+ return {
39886
+ type: "points",
39887
+ earned: 0,
39888
+ total: 1,
39889
+ message: null
39890
+ };
39891
+ }
39892
+
39715
39893
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
39716
39894
  const MovablePoint = Graphie.MovablePoint;
39717
39895
  // @ts-expect-error - TS2339 - Property 'Line' does not exist on type 'typeof Graphie'.
@@ -39837,6 +40015,9 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
39837
40015
  });
39838
40016
  return results;
39839
40017
  });
40018
+
40019
+ // TODO: most widgets use some like Widget<Something, PerseusNumberLineWidgetOptions>
40020
+ // should this one?
39840
40021
  class NumberLine extends React.Component {
39841
40022
  constructor(..._args) {
39842
40023
  super(..._args);
@@ -40116,19 +40297,21 @@ class NumberLine extends React.Component {
40116
40297
  arrows: "->"
40117
40298
  });
40118
40299
  };
40119
- this.getUserInput = () => {
40120
- return {
40121
- numLinePosition: this.props.numLinePosition,
40122
- rel: this.props.isInequality ? this.props.rel : "eq",
40123
- numDivisions: this.props.numDivisions,
40124
- divisionRange: this.props.divisionRange
40125
- };
40126
- };
40127
- this.simpleValidate = rubric => {
40128
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof NumberLine'.
40129
- return NumberLine.validate(this.getUserInput(), rubric);
40300
+ }
40301
+ static validate(state, rubric) {
40302
+ return numberLineValidator(state, rubric);
40303
+ }
40304
+ getUserInput() {
40305
+ return {
40306
+ numLinePosition: this.props.numLinePosition,
40307
+ rel: this.props.isInequality ? this.props.rel : "eq",
40308
+ numDivisions: this.props.numDivisions,
40309
+ divisionRange: this.props.divisionRange
40130
40310
  };
40131
40311
  }
40312
+ simpleValidate(rubric) {
40313
+ return numberLineValidator(this.getUserInput(), rubric);
40314
+ }
40132
40315
  render() {
40133
40316
  const {
40134
40317
  strings
@@ -40194,44 +40377,6 @@ NumberLine.defaultProps = {
40194
40377
  rel: "ge",
40195
40378
  apiOptions: ApiOptions.defaults
40196
40379
  };
40197
- _.extend(NumberLine, {
40198
- validate: function (state, rubric) {
40199
- const range = rubric.range;
40200
- const divisionRange = state.divisionRange;
40201
- const start = rubric.initialX != null ? rubric.initialX : range[0];
40202
- const startRel = rubric.isInequality ? "ge" : "eq";
40203
- const correctRel = rubric.correctRel || "eq";
40204
- const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
40205
- const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
40206
- if (state.isTickCrtl && outsideAllowedRange) {
40207
- return {
40208
- type: "invalid",
40209
- message: "Number of divisions is outside the allowed range."
40210
- };
40211
- }
40212
- if (correctPos && correctRel === state.rel) {
40213
- return {
40214
- type: "points",
40215
- earned: 1,
40216
- total: 1,
40217
- message: null
40218
- };
40219
- }
40220
- if (state.numLinePosition === start && state.rel === startRel) {
40221
- // We're where we started.
40222
- return {
40223
- type: "invalid",
40224
- message: null
40225
- };
40226
- }
40227
- return {
40228
- type: "points",
40229
- earned: 0,
40230
- total: 1,
40231
- message: null
40232
- };
40233
- }
40234
- });
40235
40380
  const numberLineTransform = editorProps => {
40236
40381
  const props = _.pick(editorProps, ["range", "labelRange", "labelStyle", "labelTicks", "divisionRange", "snapDivisions", "isTickCtrl", "isInequality", "showTooltips"]);
40237
40382
  const numLinePosition = editorProps.initialX != null ? editorProps.initialX : editorProps.range[0];
@@ -40459,8 +40604,8 @@ Card.defaultProps = {
40459
40604
  animating: false,
40460
40605
  linterContext: linterContextDefault
40461
40606
  };
40462
- const NORMAL = "normal",
40463
- HORIZONTAL = "horizontal";
40607
+ const NORMAL = "normal";
40608
+ const HORIZONTAL = "horizontal";
40464
40609
  class Orderer extends React.Component {
40465
40610
  constructor(...args) {
40466
40611
  super(...args);
@@ -40701,17 +40846,6 @@ class Orderer extends React.Component {
40701
40846
  current: list
40702
40847
  });
40703
40848
  };
40704
- this.getUserInput = () => {
40705
- return {
40706
- current: _.map(this.props.current, function (v) {
40707
- return v.content;
40708
- })
40709
- };
40710
- };
40711
- this.simpleValidate = rubric => {
40712
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Orderer'.
40713
- return Orderer.validate(this.getUserInput(), rubric);
40714
- };
40715
40849
  }
40716
40850
  UNSAFE_componentWillReceiveProps(nextProps) {
40717
40851
  if (!_.isEqual(this.props.current, nextProps.current)) {
@@ -40720,6 +40854,17 @@ class Orderer extends React.Component {
40720
40854
  });
40721
40855
  }
40722
40856
  }
40857
+ getUserInput() {
40858
+ return {
40859
+ current: _.map(this.props.current, function (v) {
40860
+ return v.content;
40861
+ })
40862
+ };
40863
+ }
40864
+ simpleValidate(rubric) {
40865
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Orderer'.
40866
+ return Orderer.validate(this.getUserInput(), rubric);
40867
+ }
40723
40868
  render() {
40724
40869
  // This is the card we are currently dragging
40725
40870
  const dragging = this.state.dragging && /*#__PURE__*/React.createElement(Card
@@ -40833,14 +40978,14 @@ Orderer.defaultProps = {
40833
40978
  linterContext: linterContextDefault
40834
40979
  };
40835
40980
  _.extend(Orderer, {
40836
- validate: function (state, rubric) {
40837
- if (state.current.length === 0) {
40981
+ validate: function (userInput, rubric) {
40982
+ if (userInput.current.length === 0) {
40838
40983
  return {
40839
40984
  type: "invalid",
40840
40985
  message: null
40841
40986
  };
40842
40987
  }
40843
- const correct = _.isEqual(state.current, _.pluck(rubric.correctOptions, "content"));
40988
+ const correct = _.isEqual(userInput.current, _.pluck(rubric.correctOptions, "content"));
40844
40989
  return {
40845
40990
  type: "points",
40846
40991
  earned: correct ? 1 : 0,
@@ -43039,10 +43184,6 @@ class PassageRefTarget extends React.Component {
43039
43184
  constructor(..._args) {
43040
43185
  super(..._args);
43041
43186
  // TODO passage-ref-target isn't interactive; remove
43042
- this.getUserInput = () => {
43043
- return;
43044
- };
43045
- // TODO passage-ref-target isn't interactive; remove
43046
43187
  this.change = (...args) => {
43047
43188
  return change.apply(this, args);
43048
43189
  };
@@ -43051,6 +43192,11 @@ class PassageRefTarget extends React.Component {
43051
43192
  static validate() {
43052
43193
  return noopValidator();
43053
43194
  }
43195
+
43196
+ // TODO passage-ref-target isn't interactive; remove
43197
+ getUserInput() {
43198
+ return null;
43199
+ }
43054
43200
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43055
43201
  simpleValidate() {
43056
43202
  return noopValidator();
@@ -43095,9 +43241,6 @@ var PassageRefTarget$1 = {
43095
43241
  * This is a PhET simulation widget. It is used for rendering simulations
43096
43242
  * from https://phet.colorado.edu/.
43097
43243
  */
43098
-
43099
- // For returning user input, but currently the PhET widget
43100
- // does not support accessing user input
43101
43244
  // This renders the PhET sim
43102
43245
  class PhetSimulation extends React.Component {
43103
43246
  constructor(props) {
@@ -43146,6 +43289,8 @@ class PhetSimulation extends React.Component {
43146
43289
  await this.updateSimState(this.props.url);
43147
43290
  }
43148
43291
  }
43292
+
43293
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43149
43294
  getUserInput() {
43150
43295
  return null;
43151
43296
  }
@@ -43297,10 +43442,25 @@ var PhetSimulation$1 = {
43297
43442
  isLintable: true
43298
43443
  };
43299
43444
 
43300
- /* eslint-disable react/no-unsafe */
43301
43445
  const {
43302
43446
  deepEq
43303
43447
  } = Util;
43448
+ function plotterValidator(userInput, rubric) {
43449
+ if (deepEq(userInput, rubric.starting)) {
43450
+ return {
43451
+ type: "invalid",
43452
+ message: null
43453
+ };
43454
+ }
43455
+ return {
43456
+ type: "points",
43457
+ earned: deepEq(userInput, rubric.correct) ? 1 : 0,
43458
+ total: 1,
43459
+ message: null
43460
+ };
43461
+ }
43462
+
43463
+ /* eslint-disable react/no-unsafe */
43304
43464
  class Plotter extends React.Component {
43305
43465
  constructor(..._args) {
43306
43466
  super(..._args);
@@ -44031,13 +44191,9 @@ class Plotter extends React.Component {
44031
44191
  });
44032
44192
  });
44033
44193
  };
44034
- this.getUserInput = () => {
44035
- return this.state.values;
44036
- };
44037
- this.simpleValidate = rubric => {
44038
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Plotter'.
44039
- return Plotter.validate(this.getUserInput(), rubric);
44040
- };
44194
+ }
44195
+ static validate(userInput, rubric) {
44196
+ return plotterValidator(userInput, rubric);
44041
44197
  }
44042
44198
  componentDidMount() {
44043
44199
  this._isMounted = true;
@@ -44062,6 +44218,12 @@ class Plotter extends React.Component {
44062
44218
  componentWillUnmount() {
44063
44219
  this._isMounted = false;
44064
44220
  }
44221
+ getUserInput() {
44222
+ return this.state.values;
44223
+ }
44224
+ simpleValidate(rubric) {
44225
+ return plotterValidator(this.getUserInput(), rubric);
44226
+ }
44065
44227
  render() {
44066
44228
  // TODO(kevinb) actually compute the size of the graphie correctly and
44067
44229
  // make it that size so we don't have to add extra padding. The value
@@ -44079,6 +44241,8 @@ class Plotter extends React.Component {
44079
44241
  });
44080
44242
  }
44081
44243
  }
44244
+
44245
+ // We don't need to change any of the original props for static mode
44082
44246
  Plotter.contextType = PerseusI18nContext;
44083
44247
  Plotter.defaultProps = {
44084
44248
  type: "bar",
@@ -44093,24 +44257,6 @@ Plotter.defaultProps = {
44093
44257
  plotDimensions: [380, 300],
44094
44258
  labelInterval: 1
44095
44259
  };
44096
- _.extend(Plotter, {
44097
- validate: function (guess, rubric) {
44098
- if (deepEq(guess, rubric.starting)) {
44099
- return {
44100
- type: "invalid",
44101
- message: null
44102
- };
44103
- }
44104
- return {
44105
- type: "points",
44106
- earned: deepEq(guess, rubric.correct) ? 1 : 0,
44107
- total: 1,
44108
- message: null
44109
- };
44110
- }
44111
- });
44112
-
44113
- // We don't need to change any of the original props for static mode
44114
44260
  const staticTransform = _.identity;
44115
44261
  var Plotter$1 = {
44116
44262
  name: "plotter",
@@ -44166,6 +44312,28 @@ var PythonProgram$1 = {
44166
44312
  widget: PythonProgram
44167
44313
  };
44168
44314
 
44315
+ function sorterValidator(userInput, rubric) {
44316
+ // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44317
+ // prevents the "Check" button from becoming active. We want the user
44318
+ // to make a change before trying to move forward. This makes an
44319
+ // assumption that the initial order isn't the correct order! However,
44320
+ // this should be rare if it happens, and interacting with the list
44321
+ // will enable the button, so they won't be locked out of progressing.
44322
+ if (!userInput.changed) {
44323
+ return {
44324
+ type: "invalid",
44325
+ message: null
44326
+ };
44327
+ }
44328
+ const correct = Util.deepEq(userInput.options, rubric.correct);
44329
+ return {
44330
+ type: "points",
44331
+ earned: correct ? 1 : 0,
44332
+ total: 1,
44333
+ message: null
44334
+ };
44335
+ }
44336
+
44169
44337
  const {
44170
44338
  shuffle
44171
44339
  } = Util;
@@ -44195,42 +44363,14 @@ class Sorter extends React.Component {
44195
44363
  }, 0);
44196
44364
  });
44197
44365
  };
44198
- this.getUserInput = () => {
44199
- // eslint-disable-next-line react/no-string-refs
44200
- // @ts-expect-error - TS2339 - Property 'getOptions' does not exist on type 'ReactInstance'.
44201
- return {
44202
- options: this.refs.sortable.getOptions()
44203
- };
44204
- };
44205
44366
  this.moveOptionToIndex = (option, index) => {
44206
44367
  // eslint-disable-next-line react/no-string-refs
44207
44368
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
44208
44369
  this.refs.sortable.moveOptionToIndex(option, index);
44209
44370
  };
44210
- this.simpleValidate = rubric => {
44211
- // If this widget hasn't been changed yet, we treat it as "empty" which
44212
- // prevents the "Check" button from becoming active. We want the user
44213
- // to make a change before trying to move forward. This makes an
44214
- // assumption that the initial order isn't the correct order! However,
44215
- // this should be rare if it happens, and interacting with the list
44216
- // will enable the button, so they won't be locked out of progressing.
44217
- if (!this.state.changed) {
44218
- return {
44219
- type: "invalid",
44220
- message: null
44221
- };
44222
- }
44223
- return Sorter.validate(this.getUserInput(), rubric);
44224
- };
44225
44371
  }
44226
- static validate(state, rubric) {
44227
- const correct = _.isEqual(state.options, rubric.correct);
44228
- return {
44229
- type: "points",
44230
- earned: correct ? 1 : 0,
44231
- total: 1,
44232
- message: null
44233
- };
44372
+ static validate(userInput, rubric) {
44373
+ return sorterValidator(userInput, rubric);
44234
44374
  }
44235
44375
  componentDidMount() {
44236
44376
  this._isMounted = true;
@@ -44238,6 +44378,18 @@ class Sorter extends React.Component {
44238
44378
  componentWillUnmount() {
44239
44379
  this._isMounted = false;
44240
44380
  }
44381
+ getUserInput() {
44382
+ // eslint-disable-next-line react/no-string-refs
44383
+ // @ts-expect-error - TS2339 - Property 'getOptions' does not exist on type 'ReactInstance'.
44384
+ const options = this.refs.sortable.getOptions();
44385
+ return {
44386
+ options,
44387
+ changed: this.state.changed
44388
+ };
44389
+ }
44390
+ simpleValidate(rubric) {
44391
+ return sorterValidator(this.getUserInput(), rubric);
44392
+ }
44241
44393
  render() {
44242
44394
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
44243
44395
  const {
@@ -44274,6 +44426,66 @@ var Sorter$1 = {
44274
44426
  isLintable: true
44275
44427
  };
44276
44428
 
44429
+ function tableValidator(state, rubric, strings) {
44430
+ const filterNonEmpty = function filterNonEmpty(table) {
44431
+ return _.filter(table, function (row) {
44432
+ // Check if row has a cell that is nonempty
44433
+ return _.some(row, _.identity);
44434
+ });
44435
+ };
44436
+ const solution = filterNonEmpty(rubric.answers);
44437
+ const supplied = filterNonEmpty(state);
44438
+ const hasEmptyCell = _.some(supplied, function (row) {
44439
+ return _.some(row, function (cell) {
44440
+ return cell === "";
44441
+ });
44442
+ });
44443
+ if (hasEmptyCell || !supplied.length) {
44444
+ return {
44445
+ type: "invalid",
44446
+ message: null
44447
+ };
44448
+ }
44449
+ if (supplied.length !== solution.length) {
44450
+ return {
44451
+ type: "points",
44452
+ earned: 0,
44453
+ total: 1,
44454
+ message: null
44455
+ };
44456
+ }
44457
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44458
+ let message = null;
44459
+ const allCorrect = _.every(solution, function (rowSolution) {
44460
+ for (let i = 0; i < supplied.length; i++) {
44461
+ const rowSupplied = supplied[i];
44462
+ const correct = _.every(rowSupplied, function (cellSupplied, i) {
44463
+ const cellSolution = rowSolution[i];
44464
+ const validator = createValidator(cellSolution, {
44465
+ simplify: true
44466
+ }, strings);
44467
+ const result = validator(cellSupplied);
44468
+ if (result.message) {
44469
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
44470
+ message = result.message;
44471
+ }
44472
+ return result.correct;
44473
+ });
44474
+ if (correct) {
44475
+ supplied.splice(i, 1);
44476
+ return true;
44477
+ }
44478
+ }
44479
+ return false;
44480
+ });
44481
+ return {
44482
+ type: "points",
44483
+ earned: allCorrect ? 1 : 0,
44484
+ total: 1,
44485
+ message: message
44486
+ };
44487
+ }
44488
+
44277
44489
  const {
44278
44490
  assert
44279
44491
  } = InteractiveUtil;
@@ -44310,9 +44522,6 @@ class Table extends React.Component {
44310
44522
  this._getColumns = () => {
44311
44523
  return this.props.answers[0].length;
44312
44524
  };
44313
- this.getUserInput = () => {
44314
- return _.map(this.props.answers, _.clone);
44315
- };
44316
44525
  this.onValueChange = (row, column, eventOrValue) => {
44317
44526
  const answers = _.map(this.props.answers, _.clone);
44318
44527
 
@@ -44332,10 +44541,6 @@ class Table extends React.Component {
44332
44541
  headers: headers
44333
44542
  });
44334
44543
  };
44335
- this.simpleValidate = rubric => {
44336
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
44337
- return Table.validate(this.getUserInput(), rubric, this.context.strings);
44338
- };
44339
44544
  this._handleFocus = inputPath => {
44340
44545
  this.props.onFocus(inputPath);
44341
44546
  };
@@ -44402,6 +44607,15 @@ class Table extends React.Component {
44402
44607
  }, cb);
44403
44608
  };
44404
44609
  }
44610
+ static validate(state, rubric, strings) {
44611
+ return tableValidator(state, rubric, strings);
44612
+ }
44613
+ getUserInput() {
44614
+ return _.map(this.props.answers, _.clone);
44615
+ }
44616
+ simpleValidate(rubric) {
44617
+ return tableValidator(this.getUserInput(), rubric, this.context.strings);
44618
+ }
44405
44619
  render() {
44406
44620
  const rows = this._getRows();
44407
44621
  const columns = this._getColumns();
@@ -44479,68 +44693,6 @@ Table.defaultProps = function () {
44479
44693
  linterContext: linterContextDefault
44480
44694
  };
44481
44695
  }();
44482
- _.extend(Table, {
44483
- validate: function (state, rubric, strings) {
44484
- const filterNonEmpty = function filterNonEmpty(table) {
44485
- return _.filter(table, function (row) {
44486
- // Check if row has a cell that is nonempty
44487
- return _.some(row, _.identity);
44488
- });
44489
- };
44490
- const solution = filterNonEmpty(rubric.answers);
44491
- const supplied = filterNonEmpty(state);
44492
- const hasEmptyCell = _.some(supplied, function (row) {
44493
- return _.some(row, function (cell) {
44494
- return cell === "";
44495
- });
44496
- });
44497
- if (hasEmptyCell || !supplied.length) {
44498
- return {
44499
- type: "invalid",
44500
- message: null
44501
- };
44502
- }
44503
- if (supplied.length !== solution.length) {
44504
- return {
44505
- type: "points",
44506
- earned: 0,
44507
- total: 1,
44508
- message: null
44509
- };
44510
- }
44511
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44512
- let message = null;
44513
- const allCorrect = _.every(solution, function (rowSolution) {
44514
- let i;
44515
- for (i = 0; i < supplied.length; i++) {
44516
- const rowSupplied = supplied[i];
44517
- const correct = _.every(rowSupplied, function (cellSupplied, i) {
44518
- const cellSolution = rowSolution[i];
44519
- const validator = createValidator(cellSolution, {
44520
- simplify: true
44521
- }, strings);
44522
- const result = validator(cellSupplied);
44523
- if (result.message) {
44524
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
44525
- message = result.message;
44526
- }
44527
- return result.correct;
44528
- });
44529
- if (correct) {
44530
- supplied.splice(i, 1);
44531
- return true;
44532
- }
44533
- }
44534
- return false;
44535
- });
44536
- return {
44537
- type: "points",
44538
- earned: allCorrect ? 1 : 0,
44539
- total: 1,
44540
- message: message
44541
- };
44542
- }
44543
- });
44544
44696
  const propTransform = editorProps => {
44545
44697
  // Remove answers before passing to widget
44546
44698
  const rows = editorProps.answers.length;
@@ -44770,7 +44922,7 @@ const init = function init(options) {
44770
44922
 
44771
44923
  // This file is processed by a Rollup plugin (replace) to inject the production
44772
44924
  const libName = "@khanacademy/perseus";
44773
- const libVersion = "34.1.0";
44925
+ const libVersion = "35.1.1";
44774
44926
  addLibraryVersionToPerseusDebug(libName, libVersion);
44775
44927
 
44776
44928
  const apiVersion = {
@@ -45321,9 +45473,6 @@ const defaultContext = {
45321
45473
  };
45322
45474
  const context = /*#__PURE__*/React.createContext(defaultContext);
45323
45475
 
45324
- const {
45325
- mapObject
45326
- } = objective_;
45327
45476
  class ServerItemRenderer extends React.Component {
45328
45477
  constructor(props) {
45329
45478
  super(props);
@@ -45543,20 +45692,6 @@ class ServerItemRenderer extends React.Component {
45543
45692
  return this.questionRenderer.getWidgetIds();
45544
45693
  }
45545
45694
 
45546
- /**
45547
- * Returns an object mapping from widget ID to KE-style score.
45548
- * The keys of this object are the values of the array returned
45549
- * from `getWidgetIds`.
45550
- */
45551
- scoreWidgets() {
45552
- const qScore = this.questionRenderer.scoreWidgets();
45553
- const qGuess = this.questionRenderer.getUserInputForWidgets();
45554
- const state = this.questionRenderer.getSerializedState();
45555
- return mapObject(qScore, (score, id) => {
45556
- return Util.keScoreFromPerseusScore(score, qGuess[id], state);
45557
- });
45558
- }
45559
-
45560
45695
  /**
45561
45696
  * Get a representation of the current state of the item.
45562
45697
  */