@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/index.js CHANGED
@@ -366,7 +366,10 @@ const mockStrings = {
366
366
  cos: "cos",
367
367
  tan: "tan",
368
368
  simulationLoadFail: "Sorry, this simulation cannot load.",
369
- simulationLocaleWarning: "Sorry, this simulation isn't available in your language."
369
+ simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
370
+ addPoint: "Add Point",
371
+ removePoint: "Remove Point",
372
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph"
370
373
  };
371
374
 
372
375
  /**
@@ -862,7 +865,7 @@ const denylist = [
862
865
  // (technically the renderer still adds them)
863
866
  "key", "ref",
864
867
  // added by src/renderer.jsx
865
- "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "questionCompleted", "findWidgets",
868
+ "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "widgetIsOpen", "questionCompleted", "findWidgets",
866
869
  // added by src/editor.jsx, for widgets removing themselves
867
870
  // this is soooo not the right place for this, but alas.
868
871
  "onRemove",
@@ -870,17 +873,6 @@ const denylist = [
870
873
  "id",
871
874
  // Callbacks and items for interaction handling
872
875
  "onBlur", "onFocus", "trackInteraction", "keypadElement"];
873
- const removeDenylistProps = props => {
874
- const newProps = {
875
- ...props
876
- };
877
- for (const prop of denylist) {
878
- if (prop in newProps) {
879
- delete newProps[prop];
880
- }
881
- }
882
- return newProps;
883
- };
884
876
 
885
877
  /* eslint-disable @babel/no-invalid-this */
886
878
  const USAGE = "Usage:\n" + " this.change({propName: 5}, callback);\n" + ' this.change("propName", 5, callback);\n' + ' this.change("propName")';
@@ -1142,8 +1134,6 @@ class StubTagEditor extends React__namespace.Component {
1142
1134
  * * nothing if it is purely a bug fix.
1143
1135
  *
1144
1136
  * Callbacks passed to Renderer/ItemRenderer:
1145
- * * onInputError:
1146
- * Called when there is an error grading a widget
1147
1137
  * * onFocusChange: (newFocusPath, oldFocusPath, keypadDOMNode)
1148
1138
  * Called when the user focus changes. The first two parameters are `path`
1149
1139
  * arrays uniquely identifying the respect inputs. The third parameter,
@@ -1166,7 +1156,6 @@ class StubTagEditor extends React__namespace.Component {
1166
1156
  const ApiOptions = {
1167
1157
  propTypes: PropTypes__default["default"].shape({
1168
1158
  isArticle: PropTypes__default["default"].bool.isRequired,
1169
- onInputError: PropTypes__default["default"].func.isRequired,
1170
1159
  onFocusChange: PropTypes__default["default"].func.isRequired,
1171
1160
  GroupMetadataEditor: PropTypes__default["default"].func.isRequired,
1172
1161
  showAlignmentOptions: PropTypes__default["default"].bool.isRequired,
@@ -1250,7 +1239,6 @@ const ApiOptions = {
1250
1239
  defaults: {
1251
1240
  isArticle: false,
1252
1241
  isMobile: false,
1253
- onInputError: function () {},
1254
1242
  onFocusChange: function () {},
1255
1243
  GroupMetadataEditor: StubTagEditor,
1256
1244
  showAlignmentOptions: false,
@@ -2262,10 +2250,7 @@ const getDecimalSeparator = locale => {
2262
2250
  * show the user an error. TODO(joel) - what error?
2263
2251
  * - Otherwise, pass through the resulting points and message.
2264
2252
  */
2265
- function expressionValidator(userInput, rubric) {
2266
- let onInputError = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};
2267
- let strings = arguments.length > 3 ? arguments[3] : undefined;
2268
- let locale = arguments.length > 4 ? arguments[4] : undefined;
2253
+ function expressionValidator(userInput, rubric, strings, locale) {
2269
2254
  const options = ___default["default"].clone(rubric);
2270
2255
  ___default["default"].extend(options, {
2271
2256
  decimal_separator: getDecimalSeparator(locale)
@@ -2365,14 +2350,9 @@ function expressionValidator(userInput, rubric) {
2365
2350
  };
2366
2351
  }
2367
2352
  if (matchingAnswerForm.considered === "ungraded") {
2368
- // We matched an ungraded answer form - return "invalid", which
2369
- // will let the user try again with no penalty
2370
- const apiResult = onInputError(null,
2371
- // Reserved for some widget identifier
2372
- userInput, matchMessage);
2373
2353
  return {
2374
2354
  type: "invalid",
2375
- message: apiResult === false ? null : matchMessage
2355
+ message: matchMessage
2376
2356
  };
2377
2357
  }
2378
2358
  // We matched a graded answer form, so we can now tell the user
@@ -2422,11 +2402,8 @@ class Expression extends React__namespace.Component {
2422
2402
  _isMounted = false;
2423
2403
 
2424
2404
  // TODO remove this in favor of just using expressionValidator
2425
- static validate(userInput, rubric) {
2426
- let onInputError = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};
2427
- let strings = arguments.length > 3 ? arguments[3] : undefined;
2428
- let locale = arguments.length > 4 ? arguments[4] : undefined;
2429
- return expressionValidator(userInput, rubric, onInputError, strings, locale);
2405
+ static validate(userInput, rubric, strings, locale) {
2406
+ return expressionValidator(userInput, rubric, strings, locale);
2430
2407
  }
2431
2408
  static getUserInputFromProps(props) {
2432
2409
  return normalizeTex(props.value);
@@ -2487,14 +2464,9 @@ class Expression extends React__namespace.Component {
2487
2464
  showErrorStyle: false
2488
2465
  });
2489
2466
  if (!this.parse(this.props.value, this.props).parsed) {
2490
- const apiResult = this.props.apiOptions.onInputError(null,
2491
- // reserved for some widget identifier
2492
- this.props.value, this.context.strings.ERROR_TITLE);
2493
- if (apiResult !== false) {
2494
- this.setState({
2495
- invalid: true
2496
- });
2497
- }
2467
+ this.setState({
2468
+ invalid: true
2469
+ });
2498
2470
  }
2499
2471
  }
2500
2472
  };
@@ -2508,12 +2480,12 @@ class Expression extends React__namespace.Component {
2508
2480
  });
2509
2481
  }
2510
2482
  };
2511
- simpleValidate = (_ref, onInputError) => {
2483
+ simpleValidate = _ref => {
2512
2484
  let {
2513
2485
  scoring,
2514
2486
  ...rubric
2515
2487
  } = _ref;
2516
- const score = expressionValidator(this.getUserInput(), rubric, onInputError || function () {}, this.context.strings, this.context.locale);
2488
+ const score = expressionValidator(this.getUserInput(), rubric, this.context.strings, this.context.locale);
2517
2489
 
2518
2490
  // "scoring" is a flag that indicates when we are checking answers.
2519
2491
  // otherwise, we may just be checking validity after changes.
@@ -2528,9 +2500,9 @@ class Expression extends React__namespace.Component {
2528
2500
  }
2529
2501
  return score;
2530
2502
  };
2531
- getUserInput = () => {
2503
+ getUserInput() {
2532
2504
  return Expression.getUserInputFromProps(this.props);
2533
- };
2505
+ }
2534
2506
  change = (() => {
2535
2507
  var _this = this;
2536
2508
  return function () {
@@ -3282,7 +3254,7 @@ function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
3282
3254
  /**
3283
3255
  * Approximate equality on numbers and primitives.
3284
3256
  */
3285
- function eq$3(x, y) {
3257
+ function eq$4(x, y) {
3286
3258
  if (typeof x === "number" && typeof y === "number") {
3287
3259
  return Math.abs(x - y) < 1e-9;
3288
3260
  }
@@ -3309,7 +3281,7 @@ function deepEq$5(x, y) {
3309
3281
  return false;
3310
3282
  }
3311
3283
  if (typeof x === "function" && typeof y === "function") {
3312
- return eq$3(x, y);
3284
+ return eq$4(x, y);
3313
3285
  }
3314
3286
  if (typeof x === "function" || typeof y === "function") {
3315
3287
  return false;
@@ -3326,7 +3298,7 @@ function deepEq$5(x, y) {
3326
3298
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
3327
3299
  return false;
3328
3300
  }
3329
- return eq$3(x, y);
3301
+ return eq$4(x, y);
3330
3302
  }
3331
3303
 
3332
3304
  /**
@@ -3624,7 +3596,7 @@ const Util = {
3624
3596
  gridStepFromTickStep,
3625
3597
  tickStepFromNumTicks,
3626
3598
  constrainedTickStepsFromTickSteps,
3627
- eq: eq$3,
3599
+ eq: eq$4,
3628
3600
  deepEq: deepEq$5,
3629
3601
  parseQueryString,
3630
3602
  updateQueryString: updateQueryString$2,
@@ -10949,7 +10921,7 @@ class WrappedLine extends WrappedDrawing {
10949
10921
  * A collection of geomtry-related utility functions
10950
10922
  */
10951
10923
  const {
10952
- eq: eq$2,
10924
+ eq: eq$3,
10953
10925
  deepEq: deepEq$4
10954
10926
  } = Util;
10955
10927
 
@@ -10959,7 +10931,7 @@ const {
10959
10931
 
10960
10932
  // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10961
10933
  function sign(val) {
10962
- if (eq$2(val, 0)) {
10934
+ if (eq$3(val, 0)) {
10963
10935
  return 0;
10964
10936
  }
10965
10937
  return val > 0 ? 1 : -1;
@@ -10971,7 +10943,7 @@ function ccw(a, b, c) {
10971
10943
  return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10972
10944
  }
10973
10945
  function collinear(a, b, c) {
10974
- return eq$2(ccw(a, b, c), 0);
10946
+ return eq$3(ccw(a, b, c), 0);
10975
10947
  }
10976
10948
 
10977
10949
  // Given rect bounding points A and B, whether point C is inside the rect
@@ -11116,7 +11088,7 @@ function similar(coords1, coords2, tolerance) {
11116
11088
  return pair[0] / pair[1];
11117
11089
  });
11118
11090
  const same = ___default["default"].all(factors, function (factor) {
11119
- return eq$2(factors[0], factor);
11091
+ return eq$3(factors[0], factor);
11120
11092
  });
11121
11093
  const congruentEnough = ___default["default"].all(sidePairs, function (pair) {
11122
11094
  return kmath.number.equal(pair[0], pair[1], tolerance);
@@ -11169,7 +11141,7 @@ function rotate(array, n) {
11169
11141
  return array.slice(n).concat(array.slice(0, n));
11170
11142
  }
11171
11143
  function getLineEquation(first, second) {
11172
- if (eq$2(first[0], second[0])) {
11144
+ if (eq$3(first[0], second[0])) {
11173
11145
  return "x = " + first[0].toFixed(3);
11174
11146
  }
11175
11147
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -14815,7 +14787,7 @@ const pluck = function (table, subKey) {
14815
14787
  * });
14816
14788
  * {a: 2, b: 3}
14817
14789
  */
14818
- const mapObject$2 = function (obj, lambda) {
14790
+ const mapObject$1 = function (obj, lambda) {
14819
14791
  const result = {};
14820
14792
  ___default["default"].each(___default["default"].keys(obj), function (key) {
14821
14793
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14841,7 +14813,7 @@ const mapObjectFromArray$1 = function (arr, lambda) {
14841
14813
  };
14842
14814
  var objective_ = {
14843
14815
  pluck: pluck,
14844
- mapObject: mapObject$2,
14816
+ mapObject: mapObject$1,
14845
14817
  mapObjectFromArray: mapObjectFromArray$1
14846
14818
  };
14847
14819
 
@@ -19789,7 +19761,7 @@ class WidgetContainer extends React__namespace.Component {
19789
19761
  }
19790
19762
 
19791
19763
  const {
19792
- mapObject: mapObject$1,
19764
+ mapObject,
19793
19765
  mapObjectFromArray
19794
19766
  } = objective_;
19795
19767
  const rContainsNonWhitespace = /\S/;
@@ -19817,9 +19789,6 @@ const isIdPathPrefix = function (prefixArray, wholeArray) {
19817
19789
  });
19818
19790
  };
19819
19791
 
19820
- // The return type for getUserInput. Widgets have full control of what is
19821
- // returned so it's not easily typed (some widgets return a scalar (string),
19822
- // some return a custom-built object
19823
19792
  /**
19824
19793
  * The Widget type represents the common API that the Renderer uses to interact
19825
19794
  * with all widgets. All widgets must implement the methods in this API, unless
@@ -19829,6 +19798,7 @@ const isIdPathPrefix = function (prefixArray, wholeArray) {
19829
19798
  * communicate with the individual widgets to coordinate actions such as
19830
19799
  * scoring, state serialization/deserialization, and focus management.
19831
19800
  */
19801
+
19832
19802
  class Renderer extends React__namespace.Component {
19833
19803
  _currentFocus;
19834
19804
  // @ts-expect-error - TS2564 - Property '_foundTextNodes' has no initializer and is not definitely assigned in the constructor.
@@ -20025,7 +19995,7 @@ class Renderer extends React__namespace.Component {
20025
19995
 
20026
19996
  // @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; }'.
20027
19997
  _getAllWidgetsInfo = props => {
20028
- return mapObject$1(props.widgets, (widgetInfo, widgetId) => {
19998
+ return mapObject(props.widgets, (widgetInfo, widgetId) => {
20029
19999
  if (!widgetInfo.type || !widgetInfo.alignment) {
20030
20000
  const newValues = {};
20031
20001
  if (!widgetInfo.type) {
@@ -20172,7 +20142,7 @@ class Renderer extends React__namespace.Component {
20172
20142
  * state from that instead of the current widget props.
20173
20143
  */
20174
20144
  getSerializedState = widgetProps => {
20175
- return mapObject$1(widgetProps || this.state.widgetProps, (props, widgetId) => {
20145
+ return mapObject(widgetProps || this.state.widgetProps, (props, widgetId) => {
20176
20146
  const widget = this.getWidgetInstance(widgetId);
20177
20147
  if (widget && widget.getSerializedState) {
20178
20148
  return widget.getSerializedState();
@@ -20208,7 +20178,7 @@ class Renderer extends React__namespace.Component {
20208
20178
  }
20209
20179
  };
20210
20180
  this.setState({
20211
- widgetProps: mapObject$1(serializedState, (props, widgetId) => {
20181
+ widgetProps: mapObject(serializedState, (props, widgetId) => {
20212
20182
  const widget = this.getWidgetInstance(widgetId);
20213
20183
  if (widget && widget.restoreSerializedState) {
20214
20184
  // Note that we probably can't call
@@ -21083,7 +21053,6 @@ class Renderer extends React__namespace.Component {
21083
21053
  */
21084
21054
  scoreWidgets = () => {
21085
21055
  const widgetProps = this.state.widgetInfo;
21086
- const onInputError = this.getApiOptions().onInputError;
21087
21056
  const gradedWidgetIds = ___default["default"].filter(this.widgetIds, id => {
21088
21057
  const props = widgetProps[id];
21089
21058
  const widgetIsGraded = props?.graded == null || props.graded;
@@ -21100,7 +21069,7 @@ class Renderer extends React__namespace.Component {
21100
21069
  widgetScores[id] = widget.simpleValidate({
21101
21070
  ...props?.options,
21102
21071
  scoring: true
21103
- }, onInputError);
21072
+ });
21104
21073
  }
21105
21074
  });
21106
21075
  return widgetScores;
@@ -21973,7 +21942,6 @@ const answerTypes = {
21973
21942
  }
21974
21943
  };
21975
21944
  function inputNumberValidator(state, rubric, strings) {
21976
- let onInputError = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : () => {};
21977
21945
  if (rubric.answerType == null) {
21978
21946
  rubric.answerType = "number";
21979
21947
  }
@@ -21997,13 +21965,9 @@ function inputNumberValidator(state, rubric, strings) {
21997
21965
  // TODO(eater): Seems silly to translate result to this invalid/points
21998
21966
  // thing and immediately translate it back in ItemRenderer.scoreInput()
21999
21967
  if (result.empty) {
22000
- // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
22001
- const apiResult = onInputError?.(null,
22002
- // reserved for some widget identifier
22003
- state.currentValue, result.message);
22004
21968
  return {
22005
21969
  type: "invalid",
22006
- message: apiResult === false ? null : result.message
21970
+ message: result.message
22007
21971
  };
22008
21972
  }
22009
21973
  return {
@@ -22056,8 +22020,7 @@ class InputNumber extends React__namespace.Component {
22056
22020
  linterContext: PerseusLinter.linterContextDefault
22057
22021
  };
22058
22022
  static validate(state, rubric, strings) {
22059
- let onInputError = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : () => {};
22060
- return inputNumberValidator(state, rubric, strings, onInputError);
22023
+ return inputNumberValidator(state, rubric, strings);
22061
22024
  }
22062
22025
  static getUserInputFromProps(props) {
22063
22026
  return {
@@ -22126,13 +22089,12 @@ class InputNumber extends React__namespace.Component {
22126
22089
  currentValue: newValue
22127
22090
  }, cb);
22128
22091
  };
22129
- getUserInput = () => {
22092
+ getUserInput() {
22130
22093
  return InputNumber.getUserInputFromProps(this.props);
22131
- };
22132
- simpleValidate = (rubric, onInputError) => {
22133
- onInputError = onInputError || function () {};
22134
- return inputNumberValidator(this.getUserInput(), rubric, this.context.strings, onInputError);
22135
- };
22094
+ }
22095
+ simpleValidate(rubric) {
22096
+ return inputNumberValidator(this.getUserInput(), rubric, this.context.strings);
22097
+ }
22136
22098
  examples = () => {
22137
22099
  const {
22138
22100
  strings
@@ -22477,9 +22439,9 @@ class NumericInput extends React__namespace.Component {
22477
22439
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22478
22440
  return !noFormsAccepted && !allFormsAccepted;
22479
22441
  };
22480
- simpleValidate = rubric => {
22442
+ simpleValidate(rubric) {
22481
22443
  return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22482
- };
22444
+ }
22483
22445
  focus = () => {
22484
22446
  this.inputRef?.focus();
22485
22447
  return true;
@@ -22506,9 +22468,9 @@ class NumericInput extends React__namespace.Component {
22506
22468
  currentValue: newValue
22507
22469
  }, cb);
22508
22470
  };
22509
- getUserInput = () => {
22471
+ getUserInput() {
22510
22472
  return NumericInput.getUserInputFromProps(this.props);
22511
- };
22473
+ }
22512
22474
  handleChange = (newValue, cb) => {
22513
22475
  this.props.onChange({
22514
22476
  currentValue: newValue
@@ -22695,11 +22657,10 @@ class PassageRef extends React__namespace.Component {
22695
22657
  this._isMounted = false;
22696
22658
  }
22697
22659
 
22698
- // TODO(tamarab): getUserInput needs to be updated to only return
22699
- // props input by the user. Currently returns all the widget's props.
22700
- getUserInput = () => {
22701
- return removeDenylistProps(this.props);
22702
- };
22660
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22661
+ getUserInput() {
22662
+ return null;
22663
+ }
22703
22664
  change = (() => {
22704
22665
  var _this = this;
22705
22666
  return function () {
@@ -24143,6 +24104,56 @@ const styles$t = aphrodite.StyleSheet.create({
24143
24104
  }
24144
24105
  });
24145
24106
 
24107
+ function radioValidator(userInput, rubric, strings) {
24108
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
24109
+ return sum + (selected ? 1 : 0);
24110
+ }, 0);
24111
+ if (numSelected === 0) {
24112
+ return {
24113
+ type: "invalid",
24114
+ message: null
24115
+ };
24116
+ }
24117
+
24118
+ // TODO: should numCorrect actually be on the rubric
24119
+ // instead of the userInput?
24120
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
24121
+ return {
24122
+ type: "invalid",
24123
+ message: strings.chooseCorrectNum
24124
+ };
24125
+ // If NOTA and some other answer are checked, ...
24126
+ }
24127
+
24128
+ // TODO: should noneOfTheAboveSelected be replaced with a
24129
+ // combination of choicesSelected and noneOfTheAboveIndex?
24130
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
24131
+ return {
24132
+ type: "invalid",
24133
+ message: strings.notNoneOfTheAbove
24134
+ };
24135
+ }
24136
+ const correct = userInput.choicesSelected.every((selected, i) => {
24137
+ let isCorrect;
24138
+ // TODO: should noneOfTheAboveIndex actually be on the rubric
24139
+ // instead of the userInput?
24140
+ if (userInput.noneOfTheAboveIndex === i) {
24141
+ isCorrect = rubric.choices.every((choice, j) => {
24142
+ return i === j || !choice.correct;
24143
+ });
24144
+ } else {
24145
+ isCorrect = !!rubric.choices[i].correct;
24146
+ }
24147
+ return isCorrect === selected;
24148
+ });
24149
+ return {
24150
+ type: "points",
24151
+ earned: correct ? 1 : 0,
24152
+ total: 1,
24153
+ message: null
24154
+ };
24155
+ }
24156
+
24146
24157
  // RenderProps is the return type for radio.jsx#transform
24147
24158
 
24148
24159
  class Radio$1 extends React__namespace.Component {
@@ -24158,46 +24169,7 @@ class Radio$1 extends React__namespace.Component {
24158
24169
  showSolutions: "none"
24159
24170
  };
24160
24171
  static validate(userInput, rubric, strings) {
24161
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
24162
- return sum + (selected ? 1 : 0);
24163
- }, 0);
24164
- if (numSelected === 0) {
24165
- return {
24166
- type: "invalid",
24167
- message: null
24168
- };
24169
- }
24170
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
24171
- return {
24172
- type: "invalid",
24173
- message: strings.chooseCorrectNum
24174
- };
24175
- // If NOTA and some other answer are checked, ...
24176
- }
24177
-
24178
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
24179
- return {
24180
- type: "invalid",
24181
- message: strings.notNoneOfTheAbove
24182
- };
24183
- }
24184
- const correct = userInput.choicesSelected.every((selected, i) => {
24185
- let isCorrect;
24186
- if (userInput.noneOfTheAboveIndex === i) {
24187
- isCorrect = rubric.choices.every((choice, j) => {
24188
- return i === j || !choice.correct;
24189
- });
24190
- } else {
24191
- isCorrect = !!rubric.choices[i].correct;
24192
- }
24193
- return isCorrect === selected;
24194
- });
24195
- return {
24196
- type: "points",
24197
- earned: correct ? 1 : 0,
24198
- total: 1,
24199
- message: null
24200
- };
24172
+ return radioValidator(userInput, rubric, strings);
24201
24173
  }
24202
24174
  static getUserInputFromProps(props) {
24203
24175
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
@@ -24382,12 +24354,12 @@ class Radio$1 extends React__namespace.Component {
24382
24354
  });
24383
24355
  this.props.trackInteraction();
24384
24356
  };
24385
- getUserInput = () => {
24357
+ getUserInput() {
24386
24358
  return Radio$1.getUserInputFromProps(this.props);
24387
- };
24388
- simpleValidate = rubric => {
24389
- return Radio$1.validate(this.getUserInput(), rubric, this.context.strings);
24390
- };
24359
+ }
24360
+ simpleValidate(rubric) {
24361
+ return radioValidator(this.getUserInput(), rubric, this.context.strings);
24362
+ }
24391
24363
 
24392
24364
  /**
24393
24365
  * Turn on rationale display for the currently selected choices. Note that
@@ -24741,9 +24713,9 @@ class Categorizer extends React__namespace.Component {
24741
24713
  return change.apply(_this, args);
24742
24714
  };
24743
24715
  })();
24744
- getUserInput = () => {
24716
+ getUserInput() {
24745
24717
  return Categorizer.getUserInputFromProps(this.props);
24746
- };
24718
+ }
24747
24719
  onChange(itemNum, catNum) {
24748
24720
  const values = [...this.props.values];
24749
24721
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -24751,9 +24723,9 @@ class Categorizer extends React__namespace.Component {
24751
24723
  this.change("values", values);
24752
24724
  this.props.trackInteraction();
24753
24725
  }
24754
- simpleValidate = rubric => {
24726
+ simpleValidate(rubric) {
24755
24727
  return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24756
- };
24728
+ }
24757
24729
  render() {
24758
24730
  const self = this;
24759
24731
 
@@ -24942,6 +24914,31 @@ const parseUrl = url => new URL(url, location().origin);
24942
24914
  */
24943
24915
  const toAbsoluteUrl = url => parseUrl(url).href;
24944
24916
 
24917
+ function csProgramValidator(state) {
24918
+ // The iframe can tell us whether it's correct or incorrect,
24919
+ // and pass an optional message
24920
+ if (state.status === "correct") {
24921
+ return {
24922
+ type: "points",
24923
+ earned: 1,
24924
+ total: 1,
24925
+ message: state.message || null
24926
+ };
24927
+ }
24928
+ if (state.status === "incorrect") {
24929
+ return {
24930
+ type: "points",
24931
+ earned: 0,
24932
+ total: 1,
24933
+ message: state.message || null
24934
+ };
24935
+ }
24936
+ return {
24937
+ type: "invalid",
24938
+ message: "Keep going, you're not there yet!"
24939
+ };
24940
+ }
24941
+
24945
24942
  /**
24946
24943
  * This widget is for embedding Khan Academy CS programs.
24947
24944
  */
@@ -24974,29 +24971,8 @@ class CSProgram extends React__namespace.Component {
24974
24971
  };
24975
24972
 
24976
24973
  // The widget's grading function
24977
- static validate(state, rubric) {
24978
- // The iframe can tell us whether it's correct or incorrect,
24979
- // and pass an optional message
24980
- if (state.status === "correct") {
24981
- return {
24982
- type: "points",
24983
- earned: 1,
24984
- total: 1,
24985
- message: state.message || null
24986
- };
24987
- }
24988
- if (state.status === "incorrect") {
24989
- return {
24990
- type: "points",
24991
- earned: 0,
24992
- total: 1,
24993
- message: state.message || null
24994
- };
24995
- }
24996
- return {
24997
- type: "invalid",
24998
- message: "Keep going, you're not there yet!"
24999
- };
24974
+ static validate(state) {
24975
+ return csProgramValidator(state);
25000
24976
  }
25001
24977
  componentDidMount() {
25002
24978
  $__default["default"](window).on("message", this.handleMessageEvent);
@@ -25033,11 +25009,11 @@ class CSProgram extends React__namespace.Component {
25033
25009
  return change.apply(_this, args);
25034
25010
  };
25035
25011
  })();
25036
- simpleValidate(rubric) {
25037
- return CSProgram.validate({
25012
+ simpleValidate() {
25013
+ return csProgramValidator({
25038
25014
  status: this.props.status,
25039
25015
  message: this.props.message
25040
- }, rubric);
25016
+ });
25041
25017
  }
25042
25018
  render() {
25043
25019
  if (!this.props.programID) {
@@ -25127,9 +25103,9 @@ class Definition extends React__namespace.Component {
25127
25103
  static validate() {
25128
25104
  return noopValidator();
25129
25105
  }
25130
- getUserInput = () => {
25106
+ getUserInput() {
25131
25107
  return {};
25132
- };
25108
+ }
25133
25109
 
25134
25110
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25135
25111
  simpleValidate() {
@@ -25272,14 +25248,14 @@ class Dropdown extends React__namespace.Component {
25272
25248
  selected: selected
25273
25249
  });
25274
25250
  };
25275
- getUserInput = () => {
25251
+ getUserInput() {
25276
25252
  return {
25277
25253
  value: this.props.selected
25278
25254
  };
25279
- };
25280
- simpleValidate = rubric => {
25255
+ }
25256
+ simpleValidate(rubric) {
25281
25257
  return dropdownValidator(this.getUserInput(), rubric);
25282
- };
25258
+ }
25283
25259
  render() {
25284
25260
  const children = [/*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.OptionItem, {
25285
25261
  key: "placeholder",
@@ -25370,9 +25346,9 @@ class Explanation extends React__namespace.Component {
25370
25346
  });
25371
25347
  this.props.trackInteraction();
25372
25348
  };
25373
- getUserInput = () => {
25349
+ getUserInput() {
25374
25350
  return {};
25375
- };
25351
+ }
25376
25352
 
25377
25353
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25378
25354
  simpleValidate() {
@@ -26640,49 +26616,6 @@ function functionForType(
26640
26616
  type) {
26641
26617
  return functionTypeMapping[type];
26642
26618
  }
26643
- const validate$1 = (state, rubric) => {
26644
- if (state.type !== rubric.correct.type) {
26645
- return {
26646
- type: "points",
26647
- earned: 0,
26648
- total: 1,
26649
- message: null
26650
- };
26651
- }
26652
-
26653
- // We haven't moved the coords
26654
- if (state.coords == null) {
26655
- return {
26656
- type: "invalid",
26657
- message: null
26658
- };
26659
- }
26660
-
26661
- // Get new function handler for grading
26662
- const grader = functionForType(state.type);
26663
- const guessCoeffs = grader.getCoefficients(state.coords, state.asymptote);
26664
- const correctCoeffs = grader.getCoefficients(rubric.correct.coords, rubric.correct.asymptote);
26665
- if (guessCoeffs == null || correctCoeffs == null) {
26666
- return {
26667
- type: "invalid",
26668
- message: null
26669
- };
26670
- }
26671
- if (grader.areEqual(guessCoeffs, correctCoeffs)) {
26672
- return {
26673
- type: "points",
26674
- earned: 1,
26675
- total: 1,
26676
- message: null
26677
- };
26678
- }
26679
- return {
26680
- type: "points",
26681
- earned: 0,
26682
- total: 1,
26683
- message: null
26684
- };
26685
- };
26686
26619
  const getEquationString = props => {
26687
26620
  const plot = props.plot;
26688
26621
  if (plot.type && plot.coords) {
@@ -26800,7 +26733,6 @@ var util = /*#__PURE__*/Object.freeze({
26800
26733
  DEFAULT_BACKGROUND_IMAGE: DEFAULT_BACKGROUND_IMAGE,
26801
26734
  allTypes: allTypes,
26802
26735
  functionForType: functionForType,
26803
- validate: validate$1,
26804
26736
  getEquationString: getEquationString,
26805
26737
  pointsFromNormalized: pointsFromNormalized,
26806
26738
  maybePointsFromNormalized: maybePointsFromNormalized,
@@ -26811,6 +26743,52 @@ var util = /*#__PURE__*/Object.freeze({
26811
26743
  typeToButton: typeToButton
26812
26744
  });
26813
26745
 
26746
+ function grapherValidator(state, rubric) {
26747
+ if (state.type !== rubric.correct.type) {
26748
+ return {
26749
+ type: "points",
26750
+ earned: 0,
26751
+ total: 1,
26752
+ message: null
26753
+ };
26754
+ }
26755
+
26756
+ // We haven't moved the coords
26757
+ if (state.coords == null) {
26758
+ return {
26759
+ type: "invalid",
26760
+ message: null
26761
+ };
26762
+ }
26763
+
26764
+ // Get new function handler for grading
26765
+ const grader = functionForType(state.type);
26766
+ const guessCoeffs = grader.getCoefficients(state.coords, state.asymptote);
26767
+ const correctCoeffs = grader.getCoefficients(rubric.correct.coords,
26768
+ // @ts-expect-error - TS(2339) - Property 'asymptote' does not exist on type '{ type: "absolute_value"; coords: [Coord, Coord]; }'
26769
+ rubric.correct.asymptote);
26770
+ if (guessCoeffs == null || correctCoeffs == null) {
26771
+ return {
26772
+ type: "invalid",
26773
+ message: null
26774
+ };
26775
+ }
26776
+ if (grader.areEqual(guessCoeffs, correctCoeffs)) {
26777
+ return {
26778
+ type: "points",
26779
+ earned: 1,
26780
+ total: 1,
26781
+ message: null
26782
+ };
26783
+ }
26784
+ return {
26785
+ type: "points",
26786
+ earned: 0,
26787
+ total: 1,
26788
+ message: null
26789
+ };
26790
+ }
26791
+
26814
26792
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
26815
26793
  const MovablePoint$3 = Graphie.MovablePoint;
26816
26794
  // @ts-expect-error - TS2339 - Property 'MovableLine' does not exist on type 'typeof Graphie'.
@@ -27008,16 +26986,13 @@ class FunctionGrapher extends React__namespace.Component {
27008
26986
  }), this.props.model && this.renderPlot(), this.props.model && this.renderAsymptote(), this.props.model && points)));
27009
26987
  }
27010
26988
  }
27011
-
27012
- // Really this is props.plot, I think
27013
-
27014
26989
  /* Widget and editor. */
27015
26990
  class Grapher extends React__namespace.Component {
27016
26991
  horizHairline;
27017
26992
  vertHairline;
27018
26993
  static defaultProps = DEFAULT_GRAPHER_PROPS;
27019
26994
  static validate(state, rubric) {
27020
- return validate$1(state, rubric);
26995
+ return grapherValidator(state, rubric);
27021
26996
  }
27022
26997
  static getUserInputFromProps(props) {
27023
26998
  return props.plot;
@@ -27122,7 +27097,7 @@ class Grapher extends React__namespace.Component {
27122
27097
  }
27123
27098
  };
27124
27099
  simpleValidate(rubric) {
27125
- return validate$1(this.getUserInput(), rubric);
27100
+ return grapherValidator(this.getUserInput(), rubric);
27126
27101
  }
27127
27102
  getUserInput() {
27128
27103
  return Grapher.getUserInputFromProps(this.props);
@@ -27245,9 +27220,9 @@ class Group extends React__namespace.Component {
27245
27220
  return change.apply(_this, args);
27246
27221
  };
27247
27222
  })();
27248
- getUserInput = () => {
27223
+ getUserInput() {
27249
27224
  return this.rendererRef?.getUserInput();
27250
- };
27225
+ }
27251
27226
  getSerializedState = () => {
27252
27227
  return this.rendererRef?.getSerializedState();
27253
27228
  };
@@ -27257,9 +27232,9 @@ class Group extends React__namespace.Component {
27257
27232
  // (all our changes were in state):
27258
27233
  return null;
27259
27234
  };
27260
- simpleValidate = rubric => {
27235
+ simpleValidate() {
27261
27236
  return this.rendererRef?.score();
27262
- };
27237
+ }
27263
27238
 
27264
27239
  // Mobile API:
27265
27240
  getInputPaths = () => {
@@ -27365,6 +27340,31 @@ var Group$1 = {
27365
27340
  isLintable: true
27366
27341
  };
27367
27342
 
27343
+ function iframeValidator(state) {
27344
+ // The iframe can tell us whether it's correct or incorrect,
27345
+ // and pass an optional message
27346
+ if (state.status === "correct") {
27347
+ return {
27348
+ type: "points",
27349
+ earned: 1,
27350
+ total: 1,
27351
+ message: state.message || null
27352
+ };
27353
+ }
27354
+ if (state.status === "incorrect") {
27355
+ return {
27356
+ type: "points",
27357
+ earned: 0,
27358
+ total: 1,
27359
+ message: state.message || null
27360
+ };
27361
+ }
27362
+ return {
27363
+ type: "invalid",
27364
+ message: "Keep going, you're not there yet!"
27365
+ };
27366
+ }
27367
+
27368
27368
  /**
27369
27369
  * This is an iframe widget. It is used for rendering an iframe that
27370
27370
  * then communicates its state via window.postMessage
@@ -27385,6 +27385,9 @@ class Iframe extends React__namespace.Component {
27385
27385
  allowFullScreen: false,
27386
27386
  allowTopNavigation: false
27387
27387
  };
27388
+ static validate(state) {
27389
+ return iframeValidator(state);
27390
+ }
27388
27391
  componentDidMount() {
27389
27392
  $__default["default"](window).on("message", this.handleMessageEvent);
27390
27393
  }
@@ -27426,10 +27429,9 @@ class Iframe extends React__namespace.Component {
27426
27429
  return change.apply(_this, args);
27427
27430
  };
27428
27431
  })();
27429
- simpleValidate = rubric => {
27430
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Iframe'.
27431
- return Iframe.validate(this.getUserInput(), rubric);
27432
- };
27432
+ simpleValidate() {
27433
+ return iframeValidator(this.getUserInput());
27434
+ }
27433
27435
  render() {
27434
27436
  const style = {
27435
27437
  width: String(this.props.width),
@@ -27492,36 +27494,6 @@ class Iframe extends React__namespace.Component {
27492
27494
  });
27493
27495
  }
27494
27496
  }
27495
-
27496
- /**
27497
- * This is the widget's grading function
27498
- */
27499
- ___default["default"].extend(Iframe, {
27500
- validate: function (state, rubric) {
27501
- // The iframe can tell us whether it's correct or incorrect,
27502
- // and pass an optional message
27503
- if (state.status === "correct") {
27504
- return {
27505
- type: "points",
27506
- earned: 1,
27507
- total: 1,
27508
- message: state.message || null
27509
- };
27510
- }
27511
- if (state.status === "incorrect") {
27512
- return {
27513
- type: "points",
27514
- earned: 0,
27515
- total: 1,
27516
- message: state.message || null
27517
- };
27518
- }
27519
- return {
27520
- type: "invalid",
27521
- message: "Keep going, you're not there yet!"
27522
- };
27523
- }
27524
- });
27525
27497
  var Iframe$1 = {
27526
27498
  name: "iframe",
27527
27499
  displayName: "Iframe (deprecated)",
@@ -27569,9 +27541,9 @@ class ImageWidget extends React__namespace.Component {
27569
27541
  return change.apply(_this, args);
27570
27542
  };
27571
27543
  })();
27572
- getUserInput = () => {
27544
+ getUserInput() {
27573
27545
  return null;
27574
- };
27546
+ }
27575
27547
 
27576
27548
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27577
27549
  simpleValidate() {
@@ -27914,13 +27886,13 @@ class Interaction extends React__namespace.Component {
27914
27886
  return change.apply(_this, args);
27915
27887
  };
27916
27888
  })();
27917
- getUserInput = () => {
27889
+ getUserInput() {
27918
27890
  // TODO(eater): Perhaps we want to be able to record the state of the
27919
27891
  // user's interaction. Unfortunately sending all the props will
27920
27892
  // probably make the attempt payload too large. So for now, don't send
27921
27893
  // anything.
27922
27894
  return {};
27923
- };
27895
+ }
27924
27896
 
27925
27897
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27926
27898
  simpleValidate() {
@@ -29054,7 +29026,8 @@ const tickLabelSize = 14;
29054
29026
  const YGridTick = _ref => {
29055
29027
  let {
29056
29028
  y,
29057
- range
29029
+ range,
29030
+ tickStep
29058
29031
  } = _ref;
29059
29032
  // If the graph requires out-of-bounds labels, we want to make sure to set the
29060
29033
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -29089,9 +29062,10 @@ const YGridTick = _ref => {
29089
29062
 
29090
29063
  // If the graph displays both the y and x axis lines within the graph, we want
29091
29064
  // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
29092
- const showLabel = shouldShowLabel(y, range);
29065
+ const showLabel = shouldShowLabel(y, range, tickStep);
29093
29066
  return /*#__PURE__*/React__namespace.createElement("g", {
29094
- className: "tick"
29067
+ className: "tick",
29068
+ "aria-hidden": true
29095
29069
  }, /*#__PURE__*/React__namespace.createElement("line", {
29096
29070
  x1: x1,
29097
29071
  y1: y1,
@@ -29151,7 +29125,8 @@ const XGridTick = _ref2 => {
29151
29125
  const xPositionText = xPosition + xAdjustment;
29152
29126
  const yPositionText = yPosition + yAdjustment;
29153
29127
  return /*#__PURE__*/React__namespace.createElement("g", {
29154
- className: "tick"
29128
+ className: "tick",
29129
+ "aria-hidden": true
29155
29130
  }, /*#__PURE__*/React__namespace.createElement("line", {
29156
29131
  x1: x1,
29157
29132
  y1: y1,
@@ -29170,13 +29145,13 @@ const XGridTick = _ref2 => {
29170
29145
  };
29171
29146
 
29172
29147
  // Determines whether to show the label for the given tick
29173
- // Currently, the only condition is to hide the label at -1
29174
- // on the y-axis when the x-axis is within the graph
29175
- const shouldShowLabel = (number, range) => {
29148
+ // Currently, the only condition is to hide the label at -tickStep
29149
+ // on the y-axis when the y-axis is within the graph bounds
29150
+ const shouldShowLabel = (currentTick, range, tickStep) => {
29176
29151
  let showLabel = true;
29177
29152
 
29178
- // If the x-axis is within the graph and the y-axis is at -1, hide the label
29179
- if (range[X][MIN] < -1 && range[X][MAX] > 0 && number === -1) {
29153
+ // If the y-axis is within the graph and currentTick equals -tickStep, hide the label
29154
+ if (range[X][MIN] < -tickStep && range[X][MAX] > 0 && currentTick === -tickStep) {
29180
29155
  showLabel = false;
29181
29156
  }
29182
29157
  return showLabel;
@@ -29218,7 +29193,8 @@ const AxisTicks = () => {
29218
29193
  return /*#__PURE__*/React__namespace.createElement(YGridTick, {
29219
29194
  y: y,
29220
29195
  key: `y-grid-tick-${y}`,
29221
- range: range
29196
+ range: range,
29197
+ tickStep: tickStep[Y]
29222
29198
  });
29223
29199
  })), /*#__PURE__*/React__namespace.createElement("g", {
29224
29200
  className: "x-axis-ticks"
@@ -29973,7 +29949,8 @@ function LockedLabel(props) {
29973
29949
  color: lockedFigureColors[color],
29974
29950
  fontSize: wonderBlocksTokens.font.size[size],
29975
29951
  backgroundColor: "rgba(255, 255, 255, 0.8)"
29976
- }
29952
+ },
29953
+ "aria-hidden": true
29977
29954
  }, /*#__PURE__*/React__namespace.createElement(TeX, null, text));
29978
29955
  }
29979
29956
 
@@ -29992,14 +29969,22 @@ function GraphLockedLabelsLayer(props) {
29992
29969
  // Point flag + point type
29993
29970
  flags?.["mafs"]?.["locked-point-labels"] && figure.type === "point" ||
29994
29971
  // Line flag + line type
29995
- flags?.["mafs"]?.["locked-line-labels"] && figure.type === "line") {
29972
+ flags?.["mafs"]?.["locked-line-labels"] && figure.type === "line" ||
29973
+ // Vector flag + vector type
29974
+ flags?.["mafs"]?.["locked-vector-labels"] && figure.type === "vector" ||
29975
+ // Ellipse flag + ellipse type
29976
+ flags?.["mafs"]?.["locked-ellipse-labels"] && figure.type === "ellipse" ||
29977
+ // Polygon flag + polygon type
29978
+ flags?.["mafs"]?.["locked-polygon-labels"] && figure.type === "polygon" ||
29979
+ // Function flag + function type
29980
+ flags?.["mafs"]?.["locked-function-labels"] && figure.type === "function") {
29996
29981
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, {
29997
29982
  key: i
29998
- }, figure.labels.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29983
+ }, figure.labels?.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29999
29984
  key: `${i}-label-${j}`
30000
- }, label))), figure.type === "line" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, figure.points[0].labels.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29985
+ }, label))), figure.type === "line" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, figure.points[0].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
30001
29986
  key: `locked-figure-${i}-point-1-label-${k}`
30002
- }, label))), figure.points[1].labels.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29987
+ }, label))), figure.points[1].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
30003
29988
  key: `locked-figure-${i}-point-2-label-${k}`
30004
29989
  }, label)))));
30005
29990
  }
@@ -30014,9 +29999,16 @@ const LockedEllipse = props => {
30014
29999
  angle,
30015
30000
  color,
30016
30001
  fillStyle,
30017
- strokeStyle
30002
+ strokeStyle,
30003
+ ariaLabel,
30004
+ flags
30018
30005
  } = props;
30019
- return /*#__PURE__*/React__namespace.createElement(mafs.Ellipse, {
30006
+ const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
30007
+ return /*#__PURE__*/React__namespace.createElement("g", {
30008
+ className: "locked-ellipse",
30009
+ "aria-label": hasAria ? ariaLabel : undefined,
30010
+ "aria-hidden": !hasAria
30011
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Ellipse, {
30020
30012
  center: center,
30021
30013
  radius: radius,
30022
30014
  angle: angle,
@@ -30032,7 +30024,7 @@ const LockedEllipse = props => {
30032
30024
  fill: fillStyle === "white" ? wonderBlocksTokens.color.white : lockedFigureColors[color]
30033
30025
  }
30034
30026
  }
30035
- });
30027
+ }));
30036
30028
  };
30037
30029
 
30038
30030
  const LockedFunction = props => {
@@ -30048,6 +30040,7 @@ const LockedFunction = props => {
30048
30040
  style: strokeStyle,
30049
30041
  domain
30050
30042
  };
30043
+ const hasAria = props.ariaLabel && props.flags?.["mafs"]?.["locked-figures-aria"];
30051
30044
  React.useEffect(() => {
30052
30045
  // Parsing the equation in a "useEffect" hook saves about 2ms each frame
30053
30046
  // when the learner is interacting with the graph (i.e. moving points).
@@ -30057,7 +30050,9 @@ const LockedFunction = props => {
30057
30050
  return null;
30058
30051
  }
30059
30052
  return /*#__PURE__*/React__namespace.createElement("g", {
30060
- className: "locked-function"
30053
+ className: "locked-function",
30054
+ "aria-label": hasAria ? props.ariaLabel : undefined,
30055
+ "aria-hidden": !hasAria
30061
30056
  }, directionalAxis === "x" && /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, _extends({
30062
30057
  y: x => equation.eval({
30063
30058
  x
@@ -30157,9 +30152,12 @@ const LockedLine = props => {
30157
30152
  points,
30158
30153
  showPoint1,
30159
30154
  showPoint2,
30155
+ ariaLabel,
30156
+ flags,
30160
30157
  range
30161
30158
  } = props;
30162
30159
  const [point1, point2] = points;
30160
+ const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
30163
30161
  let line;
30164
30162
  if (kind === "ray") {
30165
30163
  // Rays extend to the end of the graph in one direction.
@@ -30199,7 +30197,9 @@ const LockedLine = props => {
30199
30197
  }), endArrowHead);
30200
30198
  }
30201
30199
  return /*#__PURE__*/React__namespace.createElement("g", {
30202
- className: kind === "ray" ? "locked-ray" : "locked-line"
30200
+ className: kind === "ray" ? "locked-ray" : "locked-line",
30201
+ "aria-label": hasAria ? ariaLabel : undefined,
30202
+ "aria-hidden": !hasAria
30203
30203
  }, line, showPoint1 && /*#__PURE__*/React__namespace.createElement(mafs.Point, {
30204
30204
  x: point1.coord[X],
30205
30205
  y: point1.coord[Y],
@@ -30225,12 +30225,19 @@ const LockedLine = props => {
30225
30225
 
30226
30226
  const LockedPoint = props => {
30227
30227
  const {
30228
+ flags,
30228
30229
  color,
30229
30230
  coord,
30230
- filled
30231
+ filled,
30232
+ ariaLabel
30231
30233
  } = props;
30232
30234
  const [x, y] = coord;
30233
- return /*#__PURE__*/React__namespace.createElement(mafs.Point, {
30235
+ const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
30236
+ return /*#__PURE__*/React__namespace.createElement("g", {
30237
+ className: "locked-point",
30238
+ "aria-label": hasAria ? ariaLabel : undefined,
30239
+ "aria-hidden": !hasAria
30240
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Point, {
30234
30241
  x: x,
30235
30242
  y: y,
30236
30243
  svgCircleProps: {
@@ -30240,7 +30247,7 @@ const LockedPoint = props => {
30240
30247
  strokeWidth: wonderBlocksTokens.spacing.xxxxSmall_2
30241
30248
  }
30242
30249
  }
30243
- });
30250
+ }));
30244
30251
  };
30245
30252
 
30246
30253
  const LockedPolygon = props => {
@@ -30251,8 +30258,11 @@ const LockedPolygon = props => {
30251
30258
  fillStyle,
30252
30259
  strokeStyle
30253
30260
  } = props;
30261
+ const hasAria = props.ariaLabel && props.flags?.["mafs"]?.["locked-figures-aria"];
30254
30262
  return /*#__PURE__*/React__namespace.createElement("g", {
30255
- className: "locked-polygon"
30263
+ className: "locked-polygon",
30264
+ "aria-label": hasAria ? props.ariaLabel : undefined,
30265
+ "aria-hidden": !hasAria
30256
30266
  }, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
30257
30267
  points: [...points],
30258
30268
  fillOpacity: lockedFigureFillStyles[fillStyle],
@@ -30278,11 +30288,16 @@ const LockedPolygon = props => {
30278
30288
  const LockedVector = props => {
30279
30289
  const {
30280
30290
  color,
30281
- points
30291
+ points,
30292
+ ariaLabel,
30293
+ flags
30282
30294
  } = props;
30283
30295
  const [tail, tip] = points;
30296
+ const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
30284
30297
  return /*#__PURE__*/React__namespace.createElement("g", {
30285
- className: "locked-vector"
30298
+ className: "locked-vector",
30299
+ "aria-label": hasAria ? ariaLabel : undefined,
30300
+ "aria-hidden": !hasAria
30286
30301
  }, /*#__PURE__*/React__namespace.createElement(Vector, {
30287
30302
  tail: tail,
30288
30303
  tip: tip,
@@ -30292,6 +30307,7 @@ const LockedVector = props => {
30292
30307
 
30293
30308
  const GraphLockedLayer = props => {
30294
30309
  const {
30310
+ flags,
30295
30311
  lockedFigures
30296
30312
  } = props;
30297
30313
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lockedFigures.map((figure, index) => {
@@ -30299,28 +30315,40 @@ const GraphLockedLayer = props => {
30299
30315
  case "point":
30300
30316
  return /*#__PURE__*/React__namespace.createElement(LockedPoint, _extends({
30301
30317
  key: `point-${index}`
30302
- }, figure));
30318
+ }, figure, {
30319
+ flags: flags
30320
+ }));
30303
30321
  case "line":
30304
30322
  return /*#__PURE__*/React__namespace.createElement(LockedLine, _extends({
30305
30323
  key: `line-${index}`,
30306
30324
  range: props.range
30307
- }, figure));
30325
+ }, figure, {
30326
+ flags: flags
30327
+ }));
30308
30328
  case "vector":
30309
30329
  return /*#__PURE__*/React__namespace.createElement(LockedVector, _extends({
30310
30330
  key: `vector-${index}`
30311
- }, figure));
30331
+ }, figure, {
30332
+ flags: flags
30333
+ }));
30312
30334
  case "ellipse":
30313
30335
  return /*#__PURE__*/React__namespace.createElement(LockedEllipse, _extends({
30314
30336
  key: `ellipse-${index}`
30315
- }, figure));
30337
+ }, figure, {
30338
+ flags: flags
30339
+ }));
30316
30340
  case "polygon":
30317
30341
  return /*#__PURE__*/React__namespace.createElement(LockedPolygon, _extends({
30318
30342
  key: `polygon-${index}`
30319
- }, figure));
30343
+ }, figure, {
30344
+ flags: flags
30345
+ }));
30320
30346
  case "function":
30321
30347
  return /*#__PURE__*/React__namespace.createElement(LockedFunction, _extends({
30322
30348
  key: `function-${index}`
30323
- }, figure));
30349
+ }, figure, {
30350
+ flags: flags
30351
+ }));
30324
30352
  case "label":
30325
30353
  // This is rendered outside the SVG element, since
30326
30354
  // TeX cannot be rendered inside an SVG.
@@ -31807,7 +31835,7 @@ function QuadraticGraph(props) {
31807
31835
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
31808
31836
  // While we should technically never have invalid coordinates, we want to ensure that we have a fallback
31809
31837
  const coeffRef = React__namespace.useRef([0, 0, 0]);
31810
- const coeffs = getQuadraticCoefficients(coords);
31838
+ const coeffs = getQuadraticCoefficients$1(coords);
31811
31839
  if (coeffs !== undefined) {
31812
31840
  coeffRef.current = coeffs;
31813
31841
  }
@@ -31831,7 +31859,7 @@ function QuadraticGraph(props) {
31831
31859
  // These equations were originally set up in 2013 and may require some
31832
31860
  // additional comments to help clarify the quadratic formula manipulations
31833
31861
  // Origin: https://phabricator.khanacademy.org/D2413
31834
- const getQuadraticCoefficients = coords => {
31862
+ const getQuadraticCoefficients$1 = coords => {
31835
31863
  const p1 = coords[0];
31836
31864
  const p2 = coords[1];
31837
31865
  const p3 = coords[2];
@@ -31871,7 +31899,7 @@ function SinusoidGraph(props) {
31871
31899
  phase: 1,
31872
31900
  verticalOffset: 0
31873
31901
  });
31874
- const coeffs = getSinusoidCoefficients(coords);
31902
+ const coeffs = getSinusoidCoefficients$1(coords);
31875
31903
 
31876
31904
  // If the coefficients are valid, update the reference
31877
31905
  if (coeffs !== undefined) {
@@ -31900,7 +31928,7 @@ sinusoidCoefficients) {
31900
31928
  } = sinusoidCoefficients;
31901
31929
  return a * Math.sin(b * x - c) + d;
31902
31930
  };
31903
- const getSinusoidCoefficients = coords => {
31931
+ const getSinusoidCoefficients$1 = coords => {
31904
31932
  // It's assumed that p1 is the root and p2 is the first peak
31905
31933
  const p1 = coords[0];
31906
31934
  const p2 = coords[1];
@@ -32117,7 +32145,7 @@ function Protractor() {
32117
32145
  const [[xMin, xMax], [yMin, yMax]] = range;
32118
32146
  // Position the protractor in the center of the graph (horizontally), and
32119
32147
  // 95% of the way to the bottom of the graph (vertically).
32120
- const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.05)];
32148
+ const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.25)];
32121
32149
  const [center, setCenter] = React.useState(initialCenter);
32122
32150
  const [rotationHandleOffset, setRotationHandleOffset] = React.useState(centerToRotationHandle);
32123
32151
  const draggableRef = React.useRef(null);
@@ -32278,6 +32306,9 @@ const MafsGraph = props => {
32278
32306
  x: viewboxX,
32279
32307
  y: viewboxY
32280
32308
  };
32309
+ const {
32310
+ strings
32311
+ } = usePerseusI18n();
32281
32312
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
32282
32313
  value: {
32283
32314
  range: state.range,
@@ -32356,6 +32387,7 @@ const MafsGraph = props => {
32356
32387
  })),
32357
32388
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
32358
32389
  props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
32390
+ flags: props.flags,
32359
32391
  lockedFigures: props.lockedFigures,
32360
32392
  range: state.range
32361
32393
  }))), props.flags?.["mafs"]?.["interactive-graph-locked-features-labels"] && props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
@@ -32392,10 +32424,11 @@ const MafsGraph = props => {
32392
32424
  top: "50%",
32393
32425
  transform: "translateY(-50%)"
32394
32426
  }
32395
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, null, "Press ", /*#__PURE__*/React__namespace.createElement("strong", null, "Shift + Enter"), " to interact with the graph"))), renderGraphControls({
32427
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
32396
32428
  state,
32397
32429
  dispatch,
32398
- width
32430
+ width,
32431
+ strings
32399
32432
  })));
32400
32433
  };
32401
32434
  const renderPointGraphControls = props => {
@@ -32404,6 +32437,10 @@ const renderPointGraphControls = props => {
32404
32437
  showRemovePointButton,
32405
32438
  focusedPointIndex
32406
32439
  } = props.state;
32440
+ const {
32441
+ strings
32442
+ } = props;
32443
+ const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
32407
32444
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32408
32445
  style: {
32409
32446
  flexDirection: "row",
@@ -32419,7 +32456,7 @@ const renderPointGraphControls = props => {
32419
32456
  onClick: () => {
32420
32457
  props.dispatch(actions.pointGraph.addPoint([0, 0]));
32421
32458
  }
32422
- }, "Add Point"), interactionMode === "mouse" && showRemovePointButton && focusedPointIndex !== null && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32459
+ }, strings.addPoint), interactionMode === "mouse" && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32423
32460
  id: REMOVE_BUTTON_ID,
32424
32461
  kind: "secondary",
32425
32462
  color: "destructive"
@@ -32429,18 +32466,20 @@ const renderPointGraphControls = props => {
32429
32466
  tabIndex: -1,
32430
32467
  style: {
32431
32468
  width: "100%",
32432
- marginLeft: "20px"
32469
+ marginLeft: "20px",
32470
+ visibility: shouldShowRemoveButton ? "visible" : "hidden"
32433
32471
  },
32434
32472
  onClick: event => {
32435
32473
  props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32436
32474
  }
32437
- }, "Remove Point"));
32475
+ }, strings.removePoint));
32438
32476
  };
32439
32477
  const renderGraphControls = props => {
32440
32478
  const {
32441
32479
  state,
32442
32480
  dispatch,
32443
- width
32481
+ width,
32482
+ strings
32444
32483
  } = props;
32445
32484
  const {
32446
32485
  type
@@ -32451,7 +32490,8 @@ const renderGraphControls = props => {
32451
32490
  return renderPointGraphControls({
32452
32491
  state,
32453
32492
  dispatch,
32454
- width
32493
+ width,
32494
+ strings
32455
32495
  });
32456
32496
  }
32457
32497
  return null;
@@ -32603,6 +32643,95 @@ const renderGraph = props => {
32603
32643
  }
32604
32644
  };
32605
32645
 
32646
+ // Converts the state of a StatefulMafsGraph back to the format used to
32647
+ // represent graph state in the widget JSON.
32648
+ //
32649
+ // Rather than be tightly bound to how data was structured in
32650
+ // the legacy interactive graph, this lets us store state
32651
+ // however we want and we just transform it before handing it off
32652
+ // the the parent InteractiveGraph.
32653
+ //
32654
+ // The transformed data is used in the interactive graph widget editor, to
32655
+ // set the `correct` field of the graph options. In the learner-facing UI, the
32656
+ // data is also stored by the Renderer, and passed back down to the graph
32657
+ // widget via the `graph` prop. Because the data returned by this function
32658
+ // completely replaces the Renderer's representation of the widget, rather than
32659
+ // being merged into it, we take it upon ourselves to merge in the original
32660
+ // widget data here. If we didn't do this merging, the graph's configuration
32661
+ // would reset to the defaults when the learner interacted with it.
32662
+ function mafsStateToInteractiveGraph(state, originalGraph) {
32663
+ switch (state.type) {
32664
+ case "angle":
32665
+ invariant(originalGraph.type === "angle");
32666
+ return {
32667
+ ...originalGraph,
32668
+ coords: state.coords
32669
+ };
32670
+ case "quadratic":
32671
+ invariant(originalGraph.type === "quadratic");
32672
+ return {
32673
+ ...originalGraph,
32674
+ coords: state.coords
32675
+ };
32676
+ case "circle":
32677
+ invariant(originalGraph.type === "circle");
32678
+ return {
32679
+ ...originalGraph,
32680
+ center: state.center,
32681
+ radius: getRadius(state)
32682
+ };
32683
+ case "linear":
32684
+ invariant(originalGraph.type === "linear");
32685
+ return {
32686
+ ...originalGraph,
32687
+ coords: state.coords
32688
+ };
32689
+ case "ray":
32690
+ invariant(originalGraph.type === "ray");
32691
+ return {
32692
+ ...originalGraph,
32693
+ coords: state.coords
32694
+ };
32695
+ case "sinusoid":
32696
+ invariant(originalGraph.type === "sinusoid");
32697
+ return {
32698
+ ...originalGraph,
32699
+ coords: state.coords
32700
+ };
32701
+ case "segment":
32702
+ invariant(originalGraph.type === "segment");
32703
+ return {
32704
+ ...originalGraph,
32705
+ coords: state.coords
32706
+ };
32707
+ case "linear-system":
32708
+ invariant(originalGraph.type === "linear-system");
32709
+ return {
32710
+ ...originalGraph,
32711
+ coords: state.coords
32712
+ };
32713
+ case "polygon":
32714
+ invariant(originalGraph.type === "polygon");
32715
+ return {
32716
+ ...originalGraph,
32717
+ coords: state.coords
32718
+ };
32719
+ case "point":
32720
+ invariant(originalGraph.type === "point");
32721
+ return {
32722
+ ...originalGraph,
32723
+ coords: state.coords
32724
+ };
32725
+ case "none":
32726
+ invariant(originalGraph.type === "none");
32727
+ return {
32728
+ ...originalGraph
32729
+ };
32730
+ default:
32731
+ throw new wonderStuffCore.UnreachableCaseError(state);
32732
+ }
32733
+ }
32734
+
32606
32735
  function initializeGraphState(params) {
32607
32736
  const {
32608
32737
  graph,
@@ -33251,7 +33380,7 @@ function doMovePoint(state, action) {
33251
33380
  // Bind the new destination to the graph range/snapStep and then get the quadratic coefficients
33252
33381
  const boundDestination = boundAndSnapToGrid(action.destination, state);
33253
33382
  newCoords[action.index] = boundDestination;
33254
- const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
33383
+ const QuadraticCoefficients = getQuadraticCoefficients$1(newCoords);
33255
33384
 
33256
33385
  // If the new destination results in an invalid quadratic equation, we don't want to move the point
33257
33386
  if (QuadraticCoefficients === undefined) {
@@ -33407,11 +33536,11 @@ const getChange = (coords, delta, constraintOpts) => {
33407
33536
  const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
33408
33537
  return [dx, dy];
33409
33538
  };
33410
- const eq$1 = Util.eq;
33539
+ const eq$2 = Util.eq;
33411
33540
 
33412
33541
  // Less than or approximately equal
33413
33542
  function leq$1(a, b) {
33414
- return a < b || eq$1(a, b);
33543
+ return a < b || eq$2(a, b);
33415
33544
  }
33416
33545
  function boundAndSnapToGrid(point, _ref) {
33417
33546
  let {
@@ -33723,24 +33852,6 @@ function setAtIndex(args) {
33723
33852
  return copy;
33724
33853
  }
33725
33854
 
33726
- // Rather than be tightly bound to how data was structured in
33727
- // the legacy interactive graph, this lets us store state
33728
- // however we want and we just transform it before handing it off
33729
- // the the parent InteractiveGraph
33730
- function mafsStateToInteractiveGraph(state) {
33731
- if (state.graph.type === "circle") {
33732
- return {
33733
- ...state,
33734
- graph: {
33735
- ...state.graph,
33736
- radius: getRadius(state.graph)
33737
- }
33738
- };
33739
- }
33740
- return {
33741
- ...state
33742
- };
33743
- }
33744
33855
  const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
33745
33856
  const {
33746
33857
  onChange,
@@ -33753,12 +33864,12 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
33753
33864
  const prevState = React.useRef(state);
33754
33865
  React.useEffect(() => {
33755
33866
  if (prevState.current !== state) {
33756
- onChange(mafsStateToInteractiveGraph({
33757
- graph: state
33758
- }));
33867
+ onChange({
33868
+ graph: mafsStateToInteractiveGraph(state, graph)
33869
+ });
33759
33870
  }
33760
33871
  prevState.current = state;
33761
- }, [onChange, state]);
33872
+ }, [onChange, state, graph]);
33762
33873
 
33763
33874
  // Destructuring first to keep useEffect from making excess calls
33764
33875
  const [xSnap, ySnap] = props.snapStep;
@@ -33811,12 +33922,232 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
33811
33922
  }));
33812
33923
  });
33813
33924
 
33925
+ const eq$1 = Util.eq;
33926
+ const deepEq$1 = Util.deepEq;
33927
+ function interactiveGraphValidator(userInput, rubric) {
33928
+ // None-type graphs are not graded
33929
+ if (userInput.type === "none" && rubric.correct.type === "none") {
33930
+ return {
33931
+ type: "points",
33932
+ earned: 0,
33933
+ total: 0,
33934
+ message: null
33935
+ };
33936
+ }
33937
+
33938
+ // When nothing has moved, there will neither be coords nor the
33939
+ // circle's center/radius fields. When those fields are absent, skip
33940
+ // all these checks; just go mark the answer as empty.
33941
+ const hasValue = Boolean(
33942
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
33943
+ userInput.coords ||
33944
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
33945
+ userInput.center && userInput.radius);
33946
+ if (userInput.type === rubric.correct.type && hasValue) {
33947
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
33948
+ const guess = userInput.coords;
33949
+ const correct = rubric.correct.coords;
33950
+
33951
+ // If both of the guess points are on the correct line, it's
33952
+ // correct.
33953
+ if (
33954
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33955
+ collinear(correct[0], correct[1], guess[0]) &&
33956
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
33957
+ collinear(correct[0], correct[1], guess[1])) {
33958
+ return {
33959
+ type: "points",
33960
+ earned: 1,
33961
+ total: 1,
33962
+ message: null
33963
+ };
33964
+ }
33965
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
33966
+ const guess = userInput.coords;
33967
+ const correct = rubric.correct.coords;
33968
+ 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])) {
33969
+ return {
33970
+ type: "points",
33971
+ earned: 1,
33972
+ total: 1,
33973
+ message: null
33974
+ };
33975
+ }
33976
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
33977
+ // If the parabola coefficients match, it's correct.
33978
+ const guessCoeffs = getQuadraticCoefficients(userInput.coords);
33979
+ const correctCoeffs = getQuadraticCoefficients(
33980
+ // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
33981
+ rubric.correct.coords);
33982
+ if (deepEq$1(guessCoeffs, correctCoeffs)) {
33983
+ return {
33984
+ type: "points",
33985
+ earned: 1,
33986
+ total: 1,
33987
+ message: null
33988
+ };
33989
+ }
33990
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
33991
+ const guessCoeffs = getSinusoidCoefficients(userInput.coords);
33992
+ const correctCoeffs = getSinusoidCoefficients(
33993
+ // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
33994
+ rubric.correct.coords);
33995
+ const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
33996
+ const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
33997
+ // If the canonical coefficients match, it's correct.
33998
+ if (deepEq$1(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
33999
+ return {
34000
+ type: "points",
34001
+ earned: 1,
34002
+ total: 1,
34003
+ message: null
34004
+ };
34005
+ }
34006
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
34007
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
34008
+ return {
34009
+ type: "points",
34010
+ earned: 1,
34011
+ total: 1,
34012
+ message: null
34013
+ };
34014
+ }
34015
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
34016
+ let correct = rubric.correct.coords;
34017
+ if (correct == null) {
34018
+ throw new Error("Point graph rubric has null coords");
34019
+ }
34020
+ const guess = userInput.coords.slice();
34021
+ correct = correct.slice();
34022
+ // Everything's already rounded so we shouldn't need to do an
34023
+ // eq() comparison but _.isEqual(0, -0) is false, so we'll use
34024
+ // eq() anyway. The sort should be fine because it'll stringify
34025
+ // it and -0 converted to a string is "0"
34026
+ guess?.sort();
34027
+ // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
34028
+ correct.sort();
34029
+ if (deepEq$1(guess, correct)) {
34030
+ return {
34031
+ type: "points",
34032
+ earned: 1,
34033
+ total: 1,
34034
+ message: null
34035
+ };
34036
+ }
34037
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
34038
+ const guess = userInput.coords?.slice();
34039
+ // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
34040
+ const correct = rubric.correct.coords?.slice();
34041
+ let match;
34042
+ if (rubric.correct.match === "similar") {
34043
+ match = similar(guess, correct, Number.POSITIVE_INFINITY);
34044
+ } else if (rubric.correct.match === "congruent") {
34045
+ match = similar(guess, correct, kmath.number.DEFAULT_TOLERANCE);
34046
+ } else if (rubric.correct.match === "approx") {
34047
+ match = similar(guess, correct, 0.1);
34048
+ } else {
34049
+ /* exact */
34050
+ guess.sort();
34051
+ correct.sort();
34052
+ match = deepEq$1(guess, correct);
34053
+ }
34054
+ if (match) {
34055
+ return {
34056
+ type: "points",
34057
+ earned: 1,
34058
+ total: 1,
34059
+ message: null
34060
+ };
34061
+ }
34062
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
34063
+ let guess = Util.deepClone(userInput.coords);
34064
+ let correct = Util.deepClone(rubric.correct?.coords);
34065
+ guess = ___default["default"].invoke(guess, "sort").sort();
34066
+ // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
34067
+ correct = ___default["default"].invoke(correct, "sort").sort();
34068
+ if (deepEq$1(guess, correct)) {
34069
+ return {
34070
+ type: "points",
34071
+ earned: 1,
34072
+ total: 1,
34073
+ message: null
34074
+ };
34075
+ }
34076
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
34077
+ const guess = userInput.coords;
34078
+ const correct = rubric.correct.coords;
34079
+ if (
34080
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
34081
+ deepEq$1(guess[0], correct[0]) &&
34082
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
34083
+ collinear(correct[0], correct[1], guess[1])) {
34084
+ return {
34085
+ type: "points",
34086
+ earned: 1,
34087
+ total: 1,
34088
+ message: null
34089
+ };
34090
+ }
34091
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
34092
+ const guess = userInput.coords;
34093
+ const correct = rubric.correct.coords;
34094
+ let match;
34095
+ if (rubric.correct.match === "congruent") {
34096
+ const angles = ___default["default"].map([guess, correct], function (coords) {
34097
+ const angle = GraphUtils.findAngle(
34098
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
34099
+ coords[2],
34100
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
34101
+ coords[0],
34102
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
34103
+ coords[1]);
34104
+ return (angle + 360) % 360;
34105
+ });
34106
+ // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
34107
+ match = eq$1(...angles);
34108
+ } else {
34109
+ /* exact */
34110
+ match =
34111
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
34112
+ deepEq$1(guess[1], correct[1]) &&
34113
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
34114
+ collinear(correct[1], correct[0], guess[0]) &&
34115
+ // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
34116
+ collinear(correct[1], correct[2], guess[2]);
34117
+ }
34118
+ if (match) {
34119
+ return {
34120
+ type: "points",
34121
+ earned: 1,
34122
+ total: 1,
34123
+ message: null
34124
+ };
34125
+ }
34126
+ }
34127
+ }
34128
+
34129
+ // The input wasn't correct, so check if it's a blank input or if it's
34130
+ // actually just wrong
34131
+ if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
34132
+ // We're where we started.
34133
+ return {
34134
+ type: "invalid",
34135
+ message: null
34136
+ };
34137
+ }
34138
+ return {
34139
+ type: "points",
34140
+ earned: 0,
34141
+ total: 1,
34142
+ message: null
34143
+ };
34144
+ }
34145
+
33814
34146
  const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
33815
34147
  const defaultBackgroundImage = {
33816
34148
  url: null
33817
34149
  };
33818
34150
  const eq = Util.eq;
33819
- const deepEq$1 = Util.deepEq;
33820
34151
  const UNLIMITED = "unlimited";
33821
34152
 
33822
34153
  // Sample background image:
@@ -33857,6 +34188,39 @@ const makeInvalidTypeError = (functionName, graphType) => {
33857
34188
 
33858
34189
  // There's no transform function in exports
33859
34190
 
34191
+ // TODO: there's another, very similar getSinusoidCoefficients function
34192
+ // they should probably be merged
34193
+ function getSinusoidCoefficients(coords) {
34194
+ // It's assumed that p1 is the root and p2 is the first peak
34195
+ const p1 = coords[0];
34196
+ const p2 = coords[1];
34197
+
34198
+ // Resulting coefficients are canonical for this sine curve
34199
+ const amplitude = p2[1] - p1[1];
34200
+ const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
34201
+ const phase = p1[0] * angularFrequency;
34202
+ const verticalOffset = p1[1];
34203
+ return [amplitude, angularFrequency, phase, verticalOffset];
34204
+ }
34205
+
34206
+ // TODO: there's another, very similar getQuadraticCoefficients function
34207
+ // they should probably be merged
34208
+ function getQuadraticCoefficients(coords) {
34209
+ const p1 = coords[0];
34210
+ const p2 = coords[1];
34211
+ const p3 = coords[2];
34212
+ const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
34213
+ if (denom === 0) {
34214
+ // Many of the callers assume that the return value is always defined.
34215
+ // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
34216
+ return;
34217
+ }
34218
+ const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
34219
+ 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;
34220
+ 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;
34221
+ return [a, b, c];
34222
+ }
34223
+
33860
34224
  // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
33861
34225
  // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
33862
34226
  // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
@@ -34977,7 +35341,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
34977
35341
  });
34978
35342
  $__default["default"](this.angle).on("move", () => {
34979
35343
  this.onChange({
34980
- // @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'.
34981
35344
  graph: {
34982
35345
  ...graph,
34983
35346
  coords: this.angle?.getClockwiseCoords()
@@ -35004,7 +35367,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
35004
35367
  return InteractiveGraph.getUserInputFromProps(this.props);
35005
35368
  }
35006
35369
  simpleValidate(rubric) {
35007
- return InteractiveGraph.validate(this.getUserInput(), rubric, this);
35370
+ return interactiveGraphValidator(this.getUserInput(), rubric);
35008
35371
  }
35009
35372
  focus = $__default["default"].noop;
35010
35373
  render() {
@@ -35069,17 +35432,17 @@ class InteractiveGraph extends React__namespace.Component {
35069
35432
  type: "linear"
35070
35433
  }
35071
35434
  };
35072
- getUserInput = () => {
35073
- if (this.mafsRef.current) {
35074
- return this.mafsRef.current.getUserInput?.();
35435
+ getUserInput() {
35436
+ if (this.mafsRef.current?.getUserInput) {
35437
+ return this.mafsRef.current.getUserInput();
35075
35438
  }
35076
- if (this.legacyGraphRef.current) {
35439
+ if (this.legacyGraphRef.current?.getUserInput) {
35077
35440
  return this.legacyGraphRef.current.getUserInput();
35078
35441
  }
35079
35442
  throw new perseusCore.PerseusError("Cannot getUserInput from a graph that has never rendered", perseusCore.Errors.NotAllowed);
35080
- };
35443
+ }
35081
35444
  simpleValidate(rubric) {
35082
- return InteractiveGraph.validate(this.getUserInput?.(), rubric, this);
35445
+ return interactiveGraphValidator(this.getUserInput(), rubric);
35083
35446
  }
35084
35447
  render() {
35085
35448
  // Mafs shim
@@ -35102,33 +35465,6 @@ class InteractiveGraph extends React__namespace.Component {
35102
35465
  ref: this.legacyGraphRef
35103
35466
  }, this.props));
35104
35467
  }
35105
- static getQuadraticCoefficients(coords) {
35106
- const p1 = coords[0];
35107
- const p2 = coords[1];
35108
- const p3 = coords[2];
35109
- const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
35110
- if (denom === 0) {
35111
- // Many of the callers assume that the return value is always defined.
35112
- // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
35113
- return;
35114
- }
35115
- const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
35116
- 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;
35117
- 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;
35118
- return [a, b, c];
35119
- }
35120
- static getSinusoidCoefficients(coords) {
35121
- // It's assumed that p1 is the root and p2 is the first peak
35122
- const p1 = coords[0];
35123
- const p2 = coords[1];
35124
-
35125
- // Resulting coefficients are canonical for this sine curve
35126
- const amplitude = p2[1] - p1[1];
35127
- const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
35128
- const phase = p1[0] * angularFrequency;
35129
- const verticalOffset = p1[1];
35130
- return [amplitude, angularFrequency, phase, verticalOffset];
35131
- }
35132
35468
 
35133
35469
  /**
35134
35470
  * @param {object} graph Like props.graph or props.correct
@@ -35342,7 +35678,7 @@ class InteractiveGraph extends React__namespace.Component {
35342
35678
  const coords =
35343
35679
  // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35344
35680
  props.graph.coords || InteractiveGraph.defaultQuadraticCoords(props);
35345
- return InteractiveGraph.getQuadraticCoefficients(coords);
35681
+ return getQuadraticCoefficients(coords);
35346
35682
  }
35347
35683
  static defaultQuadraticCoords(props) {
35348
35684
  const coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
@@ -35357,7 +35693,7 @@ class InteractiveGraph extends React__namespace.Component {
35357
35693
  const coords =
35358
35694
  // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35359
35695
  props.graph.coords || InteractiveGraph.defaultSinusoidCoords(props);
35360
- return InteractiveGraph.getSinusoidCoefficients(coords);
35696
+ return getSinusoidCoefficients(coords);
35361
35697
  }
35362
35698
  static defaultSinusoidCoords(props) {
35363
35699
  const coords = [[0.5, 0.5], [0.65, 0.6]];
@@ -35437,220 +35773,8 @@ class InteractiveGraph extends React__namespace.Component {
35437
35773
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
35438
35774
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
35439
35775
  }
35440
- static validate(userInput, rubric, component) {
35441
- // None-type graphs are not graded
35442
- if (userInput.type === "none" && rubric.correct.type === "none") {
35443
- return {
35444
- type: "points",
35445
- earned: 0,
35446
- total: 0,
35447
- message: null
35448
- };
35449
- }
35450
-
35451
- // When nothing has moved, there will neither be coords nor the
35452
- // circle's center/radius fields. When those fields are absent, skip
35453
- // all these checks; just go mark the answer as empty.
35454
- const hasValue = Boolean(
35455
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35456
- userInput.coords ||
35457
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
35458
- userInput.center && userInput.radius);
35459
- if (userInput.type === rubric.correct.type && hasValue) {
35460
- if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
35461
- const guess = userInput.coords;
35462
- const correct = rubric.correct.coords;
35463
-
35464
- // If both of the guess points are on the correct line, it's
35465
- // correct.
35466
- if (
35467
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35468
- collinear(correct[0], correct[1], guess[0]) &&
35469
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35470
- collinear(correct[0], correct[1], guess[1])) {
35471
- return {
35472
- type: "points",
35473
- earned: 1,
35474
- total: 1,
35475
- message: null
35476
- };
35477
- }
35478
- } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
35479
- const guess = userInput.coords;
35480
- const correct = rubric.correct.coords;
35481
- 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])) {
35482
- return {
35483
- type: "points",
35484
- earned: 1,
35485
- total: 1,
35486
- message: null
35487
- };
35488
- }
35489
- } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
35490
- // If the parabola coefficients match, it's correct.
35491
- const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
35492
- const correctCoeffs = this.getQuadraticCoefficients(
35493
- // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
35494
- rubric.correct.coords);
35495
- if (deepEq$1(guessCoeffs, correctCoeffs)) {
35496
- return {
35497
- type: "points",
35498
- earned: 1,
35499
- total: 1,
35500
- message: null
35501
- };
35502
- }
35503
- } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
35504
- const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
35505
- const correctCoeffs = this.getSinusoidCoefficients(
35506
- // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
35507
- rubric.correct.coords);
35508
- const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
35509
- const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
35510
- // If the canonical coefficients match, it's correct.
35511
- if (deepEq$1(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
35512
- return {
35513
- type: "points",
35514
- earned: 1,
35515
- total: 1,
35516
- message: null
35517
- };
35518
- }
35519
- } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
35520
- if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
35521
- return {
35522
- type: "points",
35523
- earned: 1,
35524
- total: 1,
35525
- message: null
35526
- };
35527
- }
35528
- } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
35529
- let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
35530
- const guess = userInput.coords.slice();
35531
- correct = correct.slice();
35532
- // Everything's already rounded so we shouldn't need to do an
35533
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
35534
- // eq() anyway. The sort should be fine because it'll stringify
35535
- // it and -0 converted to a string is "0"
35536
- guess?.sort();
35537
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
35538
- correct.sort();
35539
- if (deepEq$1(guess, correct)) {
35540
- return {
35541
- type: "points",
35542
- earned: 1,
35543
- total: 1,
35544
- message: null
35545
- };
35546
- }
35547
- } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
35548
- const guess = userInput.coords?.slice();
35549
- // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
35550
- const correct = rubric.correct.coords?.slice();
35551
- let match;
35552
- if (rubric.correct.match === "similar") {
35553
- match = similar(guess, correct, Number.POSITIVE_INFINITY);
35554
- } else if (rubric.correct.match === "congruent") {
35555
- match = similar(guess, correct, kmath.number.DEFAULT_TOLERANCE);
35556
- } else if (rubric.correct.match === "approx") {
35557
- match = similar(guess, correct, 0.1);
35558
- } else {
35559
- /* exact */
35560
- guess.sort();
35561
- correct.sort();
35562
- match = deepEq$1(guess, correct);
35563
- }
35564
- if (match) {
35565
- return {
35566
- type: "points",
35567
- earned: 1,
35568
- total: 1,
35569
- message: null
35570
- };
35571
- }
35572
- } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
35573
- let guess = Util.deepClone(userInput.coords);
35574
- let correct = Util.deepClone(rubric.correct?.coords);
35575
- guess = ___default["default"].invoke(guess, "sort").sort();
35576
- // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
35577
- correct = ___default["default"].invoke(correct, "sort").sort();
35578
- if (deepEq$1(guess, correct)) {
35579
- return {
35580
- type: "points",
35581
- earned: 1,
35582
- total: 1,
35583
- message: null
35584
- };
35585
- }
35586
- } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
35587
- const guess = userInput.coords;
35588
- const correct = rubric.correct.coords;
35589
- if (
35590
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35591
- deepEq$1(guess[0], correct[0]) &&
35592
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35593
- collinear(correct[0], correct[1], guess[1])) {
35594
- return {
35595
- type: "points",
35596
- earned: 1,
35597
- total: 1,
35598
- message: null
35599
- };
35600
- }
35601
- } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
35602
- const guess = userInput.coords;
35603
- const correct = rubric.correct.coords;
35604
- let match;
35605
- if (rubric.correct.match === "congruent") {
35606
- const angles = ___default["default"].map([guess, correct], function (coords) {
35607
- const angle = GraphUtils.findAngle(
35608
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35609
- coords[2],
35610
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35611
- coords[0],
35612
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
35613
- coords[1]);
35614
- return (angle + 360) % 360;
35615
- });
35616
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
35617
- match = eq(...angles);
35618
- } else {
35619
- /* exact */
35620
- match =
35621
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35622
- deepEq$1(guess[1], correct[1]) &&
35623
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35624
- collinear(correct[1], correct[0], guess[0]) &&
35625
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
35626
- collinear(correct[1], correct[2], guess[2]);
35627
- }
35628
- if (match) {
35629
- return {
35630
- type: "points",
35631
- earned: 1,
35632
- total: 1,
35633
- message: null
35634
- };
35635
- }
35636
- }
35637
- }
35638
-
35639
- // The input wasn't correct, so check if it's a blank input or if it's
35640
- // actually just wrong
35641
- if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
35642
- // We're where we started.
35643
- return {
35644
- type: "invalid",
35645
- message: null
35646
- };
35647
- }
35648
- return {
35649
- type: "points",
35650
- earned: 0,
35651
- total: 1,
35652
- message: null
35653
- };
35776
+ static validate(userInput, rubric) {
35777
+ return interactiveGraphValidator(userInput, rubric);
35654
35778
  }
35655
35779
  static getUserInputFromProps(props) {
35656
35780
  return props.graph;
@@ -36746,6 +36870,57 @@ const styles$h = aphrodite.StyleSheet.create({
36746
36870
  }
36747
36871
  });
36748
36872
 
36873
+ // Question state for marker as result of user selected answers.
36874
+
36875
+ function scoreMarker(marker) {
36876
+ const score = {
36877
+ hasAnswers: false,
36878
+ isCorrect: false
36879
+ };
36880
+ if (marker.selected && marker.selected.length > 0) {
36881
+ score.hasAnswers = true;
36882
+ }
36883
+ if (marker.answers.length > 0) {
36884
+ if (marker.selected && marker.selected.length === marker.answers.length) {
36885
+ // All correct answers are selected by the user.
36886
+ score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36887
+ }
36888
+ } else if (!marker.selected || marker.selected.length === 0) {
36889
+ // Correct as no answers should be selected by the user.
36890
+ score.isCorrect = true;
36891
+ }
36892
+ return score;
36893
+ }
36894
+ function labelImageValidator(state, rubric) {
36895
+ let numAnswered = 0;
36896
+ let numCorrect = 0;
36897
+ for (const marker of state.markers) {
36898
+ const score = scoreMarker(marker);
36899
+ if (score.hasAnswers) {
36900
+ numAnswered++;
36901
+ }
36902
+ if (score.isCorrect) {
36903
+ numCorrect++;
36904
+ }
36905
+ }
36906
+
36907
+ // We expect all question markers to be answered before grading.
36908
+ if (numAnswered !== state.markers.length) {
36909
+ return {
36910
+ type: "invalid",
36911
+ message: null
36912
+ };
36913
+ }
36914
+ return {
36915
+ type: "points",
36916
+ // Markers with no expected answers are graded as correct if user
36917
+ // makes no answer selection.
36918
+ earned: numCorrect === state.markers.length ? 1 : 0,
36919
+ total: 1,
36920
+ message: null
36921
+ };
36922
+ }
36923
+
36749
36924
  const BringToFront = {
36750
36925
  boxShadow: `0 8px 8px ${wonderBlocksTokens.color.offBlack64}`,
36751
36926
  zIndex: 1000
@@ -37006,61 +37181,15 @@ const styles$f = aphrodite.StyleSheet.create({
37006
37181
  * No diagonals allowed.
37007
37182
  */
37008
37183
 
37184
+ // TODO: should this be using WidgetProps / PerseusLabelImageWidgetOptions?
37185
+
37009
37186
  class LabelImage extends React__namespace.Component {
37010
37187
  static contextType = PerseusI18nContext;
37011
37188
  // The rendered markers on the question image for labeling.
37012
37189
  _markers;
37013
37190
  _mounted = false;
37014
- static gradeMarker(marker) {
37015
- const score = {
37016
- hasAnswers: false,
37017
- isCorrect: false
37018
- };
37019
- if (marker.selected && marker.selected.length > 0) {
37020
- // @ts-expect-error - TS2540 - Cannot assign to 'hasAnswers' because it is a read-only property.
37021
- score.hasAnswers = true;
37022
- }
37023
- if (marker.answers.length > 0) {
37024
- if (marker.selected && marker.selected.length === marker.answers.length) {
37025
- // All correct answers are selected by the user.
37026
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
37027
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
37028
- }
37029
- } else if (!marker.selected || marker.selected.length === 0) {
37030
- // Correct as no answers should be selected by the user.
37031
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
37032
- score.isCorrect = true;
37033
- }
37034
- return score;
37035
- }
37036
37191
  static validate(state, rubric) {
37037
- let numAnswered = 0;
37038
- let numCorrect = 0;
37039
- for (const marker of state.markers) {
37040
- const score = LabelImage.gradeMarker(marker);
37041
- if (score.hasAnswers) {
37042
- numAnswered++;
37043
- }
37044
- if (score.isCorrect) {
37045
- numCorrect++;
37046
- }
37047
- }
37048
-
37049
- // We expect all question markers to be answered before grading.
37050
- if (numAnswered !== state.markers.length) {
37051
- return {
37052
- type: "invalid",
37053
- message: null
37054
- };
37055
- }
37056
- return {
37057
- type: "points",
37058
- // Markers with no expected answers are graded as correct if user
37059
- // makes no answer selection.
37060
- earned: numCorrect === state.markers.length ? 1 : 0,
37061
- total: 1,
37062
- message: null
37063
- };
37192
+ return labelImageValidator(state);
37064
37193
  }
37065
37194
 
37066
37195
  /**
@@ -37255,7 +37384,7 @@ class LabelImage extends React__namespace.Component {
37255
37384
  this._mounted = false;
37256
37385
  }
37257
37386
  simpleValidate(rubric) {
37258
- return LabelImage.validate(this.getUserInput(), rubric);
37387
+ return labelImageValidator(this.getUserInput());
37259
37388
  }
37260
37389
  getUserInput() {
37261
37390
  const {
@@ -37273,7 +37402,7 @@ class LabelImage extends React__namespace.Component {
37273
37402
  onChange
37274
37403
  } = this.props;
37275
37404
  const updatedMarkers = markers.map(marker => {
37276
- const score = LabelImage.gradeMarker(marker);
37405
+ const score = scoreMarker(marker);
37277
37406
  return {
37278
37407
  ...marker,
37279
37408
  // Reveal correctness state for markers with answers.
@@ -37422,7 +37551,7 @@ class LabelImage extends React__namespace.Component {
37422
37551
  bottom: "top"
37423
37552
  }[markerPosition];
37424
37553
  }
37425
- const score = LabelImage.gradeMarker(marker);
37554
+ const score = scoreMarker(marker);
37426
37555
  // Once the question is answered, show markers
37427
37556
  // with correct answers, otherwise passthrough
37428
37557
  // the correctness state.
@@ -37628,9 +37757,7 @@ const LabelImageWithDependencies = /*#__PURE__*/React__namespace.forwardRef((pro
37628
37757
  // methods and instead adjust Peresus to provide these facilities through
37629
37758
  // instance methods on our Renderers.
37630
37759
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
37631
- LabelImageWithDependencies.validate = LabelImage.validate;
37632
- // @ts-expect-error - TS2339 - Property 'gradeMarker' does not exist on type
37633
- LabelImageWithDependencies.gradeMarker = LabelImage.gradeMarker;
37760
+ LabelImageWithDependencies.validate = labelImageValidator;
37634
37761
  var LabelImage$1 = {
37635
37762
  name: "label-image",
37636
37763
  displayName: "Label Image",
@@ -38346,6 +38473,16 @@ const styles$d = aphrodite.StyleSheet.create({
38346
38473
  }
38347
38474
  });
38348
38475
 
38476
+ function matcherValidator(state, rubric) {
38477
+ const correct = ___default["default"].isEqual(state.left, rubric.left) && ___default["default"].isEqual(state.right, rubric.right);
38478
+ return {
38479
+ type: "points",
38480
+ earned: correct ? 1 : 0,
38481
+ total: 1,
38482
+ message: null
38483
+ };
38484
+ }
38485
+
38349
38486
  const {
38350
38487
  shuffle: shuffle$1,
38351
38488
  seededRNG
@@ -38368,6 +38505,9 @@ class Matcher extends React__namespace.Component {
38368
38505
  rightHeight: 0,
38369
38506
  texRendererLoaded: false
38370
38507
  };
38508
+ static validate(state, rubric) {
38509
+ return matcherValidator(state, rubric);
38510
+ }
38371
38511
  changeAndTrack = e => {
38372
38512
  this.props.onChange(e);
38373
38513
  this.props.trackInteraction();
@@ -38419,10 +38559,9 @@ class Matcher extends React__namespace.Component {
38419
38559
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
38420
38560
  this.refs.right.moveOptionToIndex(option, index);
38421
38561
  };
38422
- simpleValidate = rubric => {
38423
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matcher'.
38424
- return Matcher.validate(this.getUserInput(), rubric);
38425
- };
38562
+ simpleValidate(rubric) {
38563
+ return matcherValidator(this.getUserInput(), rubric);
38564
+ }
38426
38565
  render() {
38427
38566
  // To minimize layout shift, we display a spinner until our math
38428
38567
  // renderer is ready to render the math inside the matcher. To
@@ -38517,17 +38656,6 @@ class Matcher extends React__namespace.Component {
38517
38656
  })))));
38518
38657
  }
38519
38658
  }
38520
- ___default["default"].extend(Matcher, {
38521
- validate: function (state, rubric) {
38522
- const correct = ___default["default"].isEqual(state.left, rubric.left) && ___default["default"].isEqual(state.right, rubric.right);
38523
- return {
38524
- type: "points",
38525
- earned: correct ? 1 : 0,
38526
- total: 1,
38527
- message: null
38528
- };
38529
- }
38530
- });
38531
38659
  const padding = 5;
38532
38660
  const border = "1px solid #444";
38533
38661
  const styles$c = aphrodite.StyleSheet.create({
@@ -38563,7 +38691,59 @@ var Matcher$1 = {
38563
38691
  isLintable: true
38564
38692
  };
38565
38693
 
38566
- // Type imports
38694
+ function matrixValidator(state, rubric, strings) {
38695
+ const solution = rubric.answers;
38696
+ const supplied = state.answers;
38697
+ const solutionSize = getMatrixSize(solution);
38698
+ const suppliedSize = getMatrixSize(supplied);
38699
+ const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
38700
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
38701
+ let message = null;
38702
+ let hasEmptyCell = false;
38703
+ let incorrect = false;
38704
+ ___default["default"](suppliedSize[0]).times(row => {
38705
+ ___default["default"](suppliedSize[1]).times(col => {
38706
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
38707
+ hasEmptyCell = true;
38708
+ }
38709
+ if (!incorrectSize) {
38710
+ const validator = createValidator(
38711
+ // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
38712
+ solution[row][col], {
38713
+ simplify: true
38714
+ }, strings);
38715
+ const result = validator(supplied[row][col]);
38716
+ if (result.message) {
38717
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
38718
+ message = result.message;
38719
+ }
38720
+ if (!result.correct) {
38721
+ incorrect = true;
38722
+ }
38723
+ }
38724
+ });
38725
+ });
38726
+ if (hasEmptyCell) {
38727
+ return {
38728
+ type: "invalid",
38729
+ message: strings.fillAllCells
38730
+ };
38731
+ }
38732
+ if (incorrectSize) {
38733
+ return {
38734
+ type: "points",
38735
+ earned: 0,
38736
+ total: 1,
38737
+ message: null
38738
+ };
38739
+ }
38740
+ return {
38741
+ type: "points",
38742
+ earned: incorrect ? 0 : 1,
38743
+ total: 1,
38744
+ message: message
38745
+ };
38746
+ }
38567
38747
 
38568
38748
  const {
38569
38749
  assert: assert$2
@@ -38613,7 +38793,7 @@ const getRefForPath$1 = function (path) {
38613
38793
  const column = getColumnFromPath$1(path);
38614
38794
  return "answer" + row + "," + column;
38615
38795
  };
38616
- const getMatrixSize = function (matrix) {
38796
+ function getMatrixSize(matrix) {
38617
38797
  const matrixSize = [1, 1];
38618
38798
 
38619
38799
  // We need to find the widest row and tallest column to get the correct
@@ -38635,7 +38815,7 @@ const getMatrixSize = function (matrix) {
38635
38815
  }
38636
38816
  });
38637
38817
  return matrixSize;
38638
- };
38818
+ }
38639
38819
  class Matrix extends React__namespace.Component {
38640
38820
  static contextType = PerseusI18nContext;
38641
38821
  // @ts-expect-error - TS2564 - Property 'cursorPosition' has no initializer and is not definitely assigned in the constructor.
@@ -38650,55 +38830,7 @@ class Matrix extends React__namespace.Component {
38650
38830
  linterContext: PerseusLinter.linterContextDefault
38651
38831
  };
38652
38832
  static validate(state, rubric, strings) {
38653
- const solution = rubric.answers;
38654
- const supplied = state.answers;
38655
- const solutionSize = getMatrixSize(solution);
38656
- const suppliedSize = getMatrixSize(supplied);
38657
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
38658
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
38659
- let message = null;
38660
- let hasEmptyCell = false;
38661
- let incorrect = false;
38662
- ___default["default"](suppliedSize[0]).times(row => {
38663
- ___default["default"](suppliedSize[1]).times(col => {
38664
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
38665
- hasEmptyCell = true;
38666
- }
38667
- if (!incorrectSize) {
38668
- const validator = createValidator(solution[row][col], {
38669
- simplify: true
38670
- }, strings);
38671
- const result = validator(supplied[row][col]);
38672
- if (result.message) {
38673
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
38674
- message = result.message;
38675
- }
38676
- if (!result.correct) {
38677
- incorrect = true;
38678
- }
38679
- }
38680
- });
38681
- });
38682
- if (hasEmptyCell) {
38683
- return {
38684
- type: "invalid",
38685
- message: strings.fillAllCells
38686
- };
38687
- }
38688
- if (incorrectSize) {
38689
- return {
38690
- type: "points",
38691
- earned: 0,
38692
- total: 1,
38693
- message: null
38694
- };
38695
- }
38696
- return {
38697
- type: "points",
38698
- earned: incorrect ? 0 : 1,
38699
- total: 1,
38700
- message: message
38701
- };
38833
+ return matrixValidator(state, rubric, strings);
38702
38834
  }
38703
38835
  state = {
38704
38836
  enterTheMatrix: 0
@@ -38837,14 +38969,14 @@ class Matrix extends React__namespace.Component {
38837
38969
  }, cb);
38838
38970
  this.props.trackInteraction();
38839
38971
  };
38840
- getUserInput = () => {
38972
+ getUserInput() {
38841
38973
  return {
38842
38974
  answers: this.props.answers
38843
38975
  };
38844
- };
38845
- simpleValidate = rubric => {
38846
- return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
38847
- };
38976
+ }
38977
+ simpleValidate(rubric) {
38978
+ return matrixValidator(this.getUserInput(), rubric, this.context.strings);
38979
+ }
38848
38980
  render() {
38849
38981
  // Set the input sizes through JS so we can control the size of the
38850
38982
  // brackets. (If we set them in CSS we won't know values until the
@@ -40057,9 +40189,9 @@ class MoleculeWidget extends React__namespace.Component {
40057
40189
  simpleValidate() {
40058
40190
  return noopValidator();
40059
40191
  }
40060
- getUserInput = () => {
40192
+ getUserInput() {
40061
40193
  return [];
40062
- };
40194
+ }
40063
40195
 
40064
40196
  // TODO (LEMS-2396): remove validation logic from widgets that don't validate
40065
40197
  validate() {
@@ -40080,6 +40212,45 @@ var Molecule$1 = {
40080
40212
  widget: MoleculeWidget
40081
40213
  };
40082
40214
 
40215
+ function numberLineValidator(state, rubric) {
40216
+ const range = rubric.range;
40217
+ const divisionRange = state.divisionRange;
40218
+ const start = rubric.initialX != null ? rubric.initialX : range[0];
40219
+ const startRel = rubric.isInequality ? "ge" : "eq";
40220
+ const correctRel = rubric.correctRel || "eq";
40221
+ const correctPos = kmath.number.equal(state.numLinePosition, rubric.correctX || 0);
40222
+ const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
40223
+
40224
+ // TODO: I don't think isTickCrtl is a thing anymore
40225
+ if (state.isTickCrtl && outsideAllowedRange) {
40226
+ return {
40227
+ type: "invalid",
40228
+ message: "Number of divisions is outside the allowed range."
40229
+ };
40230
+ }
40231
+ if (correctPos && correctRel === state.rel) {
40232
+ return {
40233
+ type: "points",
40234
+ earned: 1,
40235
+ total: 1,
40236
+ message: null
40237
+ };
40238
+ }
40239
+ if (state.numLinePosition === start && state.rel === startRel) {
40240
+ // We're where we started.
40241
+ return {
40242
+ type: "invalid",
40243
+ message: null
40244
+ };
40245
+ }
40246
+ return {
40247
+ type: "points",
40248
+ earned: 0,
40249
+ total: 1,
40250
+ message: null
40251
+ };
40252
+ }
40253
+
40083
40254
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
40084
40255
  const MovablePoint = Graphie.MovablePoint;
40085
40256
  // @ts-expect-error - TS2339 - Property 'Line' does not exist on type 'typeof Graphie'.
@@ -40205,6 +40376,9 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
40205
40376
  });
40206
40377
  return results;
40207
40378
  });
40379
+
40380
+ // TODO: most widgets use some like Widget<Something, PerseusNumberLineWidgetOptions>
40381
+ // should this one?
40208
40382
  class NumberLine extends React__namespace.Component {
40209
40383
  static contextType = PerseusI18nContext;
40210
40384
  static defaultProps = {
@@ -40224,6 +40398,9 @@ class NumberLine extends React__namespace.Component {
40224
40398
  state = {
40225
40399
  numDivisionsEmpty: false
40226
40400
  };
40401
+ static validate(state, rubric) {
40402
+ return numberLineValidator(state, rubric);
40403
+ }
40227
40404
  change = (() => {
40228
40405
  var _this = this;
40229
40406
  return function () {
@@ -40506,18 +40683,17 @@ class NumberLine extends React__namespace.Component {
40506
40683
  arrows: "->"
40507
40684
  });
40508
40685
  };
40509
- getUserInput = () => {
40686
+ getUserInput() {
40510
40687
  return {
40511
40688
  numLinePosition: this.props.numLinePosition,
40512
40689
  rel: this.props.isInequality ? this.props.rel : "eq",
40513
40690
  numDivisions: this.props.numDivisions,
40514
40691
  divisionRange: this.props.divisionRange
40515
40692
  };
40516
- };
40517
- simpleValidate = rubric => {
40518
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof NumberLine'.
40519
- return NumberLine.validate(this.getUserInput(), rubric);
40520
- };
40693
+ }
40694
+ simpleValidate(rubric) {
40695
+ return numberLineValidator(this.getUserInput(), rubric);
40696
+ }
40521
40697
  render() {
40522
40698
  const {
40523
40699
  strings
@@ -40568,44 +40744,6 @@ class NumberLine extends React__namespace.Component {
40568
40744
  })) : this._renderGraphie(), !this.props.static && this.props.isInequality && inequalityControls);
40569
40745
  }
40570
40746
  }
40571
- ___default["default"].extend(NumberLine, {
40572
- validate: function (state, rubric) {
40573
- const range = rubric.range;
40574
- const divisionRange = state.divisionRange;
40575
- const start = rubric.initialX != null ? rubric.initialX : range[0];
40576
- const startRel = rubric.isInequality ? "ge" : "eq";
40577
- const correctRel = rubric.correctRel || "eq";
40578
- const correctPos = kmath.number.equal(state.numLinePosition, rubric.correctX || 0);
40579
- const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
40580
- if (state.isTickCrtl && outsideAllowedRange) {
40581
- return {
40582
- type: "invalid",
40583
- message: "Number of divisions is outside the allowed range."
40584
- };
40585
- }
40586
- if (correctPos && correctRel === state.rel) {
40587
- return {
40588
- type: "points",
40589
- earned: 1,
40590
- total: 1,
40591
- message: null
40592
- };
40593
- }
40594
- if (state.numLinePosition === start && state.rel === startRel) {
40595
- // We're where we started.
40596
- return {
40597
- type: "invalid",
40598
- message: null
40599
- };
40600
- }
40601
- return {
40602
- type: "points",
40603
- earned: 0,
40604
- total: 1,
40605
- message: null
40606
- };
40607
- }
40608
- });
40609
40747
  const numberLineTransform = editorProps => {
40610
40748
  const props = ___default["default"].pick(editorProps, ["range", "labelRange", "labelStyle", "labelTicks", "divisionRange", "snapDivisions", "isTickCtrl", "isInequality", "showTooltips"]);
40611
40749
  const numLinePosition = editorProps.initialX != null ? editorProps.initialX : editorProps.range[0];
@@ -40829,8 +40967,8 @@ class Card extends React__namespace.Component {
40829
40967
  }))));
40830
40968
  }
40831
40969
  }
40832
- const NORMAL = "normal",
40833
- HORIZONTAL = "horizontal";
40970
+ const NORMAL = "normal";
40971
+ const HORIZONTAL = "horizontal";
40834
40972
  class Orderer extends React__namespace.Component {
40835
40973
  static defaultProps = {
40836
40974
  current: [],
@@ -41085,17 +41223,17 @@ class Orderer extends React__namespace.Component {
41085
41223
  current: list
41086
41224
  });
41087
41225
  };
41088
- getUserInput = () => {
41226
+ getUserInput() {
41089
41227
  return {
41090
41228
  current: ___default["default"].map(this.props.current, function (v) {
41091
41229
  return v.content;
41092
41230
  })
41093
41231
  };
41094
- };
41095
- simpleValidate = rubric => {
41232
+ }
41233
+ simpleValidate(rubric) {
41096
41234
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Orderer'.
41097
41235
  return Orderer.validate(this.getUserInput(), rubric);
41098
- };
41236
+ }
41099
41237
  render() {
41100
41238
  // This is the card we are currently dragging
41101
41239
  const dragging = this.state.dragging && /*#__PURE__*/React__namespace.createElement(Card
@@ -41201,14 +41339,14 @@ class Orderer extends React__namespace.Component {
41201
41339
  }
41202
41340
  }
41203
41341
  ___default["default"].extend(Orderer, {
41204
- validate: function (state, rubric) {
41205
- if (state.current.length === 0) {
41342
+ validate: function (userInput, rubric) {
41343
+ if (userInput.current.length === 0) {
41206
41344
  return {
41207
41345
  type: "invalid",
41208
41346
  message: null
41209
41347
  };
41210
41348
  }
41211
- const correct = ___default["default"].isEqual(state.current, ___default["default"].pluck(rubric.correctOptions, "content"));
41349
+ const correct = ___default["default"].isEqual(userInput.current, ___default["default"].pluck(rubric.correctOptions, "content"));
41212
41350
  return {
41213
41351
  type: "points",
41214
41352
  earned: correct ? 1 : 0,
@@ -43418,9 +43556,9 @@ class PassageRefTarget extends React__namespace.Component {
43418
43556
  }
43419
43557
 
43420
43558
  // TODO passage-ref-target isn't interactive; remove
43421
- getUserInput = () => {
43422
- return;
43423
- };
43559
+ getUserInput() {
43560
+ return null;
43561
+ }
43424
43562
 
43425
43563
  // TODO passage-ref-target isn't interactive; remove
43426
43564
  change = (() => {
@@ -43472,9 +43610,6 @@ var PassageRefTarget$1 = {
43472
43610
  * This is a PhET simulation widget. It is used for rendering simulations
43473
43611
  * from https://phet.colorado.edu/.
43474
43612
  */
43475
-
43476
- // For returning user input, but currently the PhET widget
43477
- // does not support accessing user input
43478
43613
  // This renders the PhET sim
43479
43614
  class PhetSimulation extends React__namespace.Component {
43480
43615
  static contextType = PerseusI18nContext;
@@ -43497,6 +43632,8 @@ class PhetSimulation extends React__namespace.Component {
43497
43632
  await this.updateSimState(this.props.url);
43498
43633
  }
43499
43634
  }
43635
+
43636
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43500
43637
  getUserInput() {
43501
43638
  return null;
43502
43639
  }
@@ -43673,10 +43810,25 @@ var PhetSimulation$1 = {
43673
43810
  isLintable: true
43674
43811
  };
43675
43812
 
43676
- /* eslint-disable react/no-unsafe */
43677
43813
  const {
43678
43814
  deepEq
43679
43815
  } = Util;
43816
+ function plotterValidator(userInput, rubric) {
43817
+ if (deepEq(userInput, rubric.starting)) {
43818
+ return {
43819
+ type: "invalid",
43820
+ message: null
43821
+ };
43822
+ }
43823
+ return {
43824
+ type: "points",
43825
+ earned: deepEq(userInput, rubric.correct) ? 1 : 0,
43826
+ total: 1,
43827
+ message: null
43828
+ };
43829
+ }
43830
+
43831
+ /* eslint-disable react/no-unsafe */
43680
43832
  class Plotter extends React__namespace.Component {
43681
43833
  static contextType = PerseusI18nContext;
43682
43834
  // @ts-expect-error - TS2564 - Property 'shouldSetupGraphie' has no initializer and is not definitely assigned in the constructor.
@@ -43705,6 +43857,9 @@ class Plotter extends React__namespace.Component {
43705
43857
  // bottom label.
43706
43858
  categoryHeights: {}
43707
43859
  };
43860
+ static validate(userInput, rubric) {
43861
+ return plotterValidator(userInput, rubric);
43862
+ }
43708
43863
  componentDidMount() {
43709
43864
  this._isMounted = true;
43710
43865
  this.setupGraphie(this.state);
@@ -44446,13 +44601,12 @@ class Plotter extends React__namespace.Component {
44446
44601
  });
44447
44602
  });
44448
44603
  };
44449
- getUserInput = () => {
44604
+ getUserInput() {
44450
44605
  return this.state.values;
44451
- };
44452
- simpleValidate = rubric => {
44453
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Plotter'.
44454
- return Plotter.validate(this.getUserInput(), rubric);
44455
- };
44606
+ }
44607
+ simpleValidate(rubric) {
44608
+ return plotterValidator(this.getUserInput(), rubric);
44609
+ }
44456
44610
  render() {
44457
44611
  // TODO(kevinb) actually compute the size of the graphie correctly and
44458
44612
  // make it that size so we don't have to add extra padding. The value
@@ -44470,22 +44624,6 @@ class Plotter extends React__namespace.Component {
44470
44624
  });
44471
44625
  }
44472
44626
  }
44473
- ___default["default"].extend(Plotter, {
44474
- validate: function (guess, rubric) {
44475
- if (deepEq(guess, rubric.starting)) {
44476
- return {
44477
- type: "invalid",
44478
- message: null
44479
- };
44480
- }
44481
- return {
44482
- type: "points",
44483
- earned: deepEq(guess, rubric.correct) ? 1 : 0,
44484
- total: 1,
44485
- message: null
44486
- };
44487
- }
44488
- });
44489
44627
 
44490
44628
  // We don't need to change any of the original props for static mode
44491
44629
  const staticTransform = ___default["default"].identity;
@@ -44543,6 +44681,28 @@ var PythonProgram$1 = {
44543
44681
  widget: PythonProgram
44544
44682
  };
44545
44683
 
44684
+ function sorterValidator(userInput, rubric) {
44685
+ // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44686
+ // prevents the "Check" button from becoming active. We want the user
44687
+ // to make a change before trying to move forward. This makes an
44688
+ // assumption that the initial order isn't the correct order! However,
44689
+ // this should be rare if it happens, and interacting with the list
44690
+ // will enable the button, so they won't be locked out of progressing.
44691
+ if (!userInput.changed) {
44692
+ return {
44693
+ type: "invalid",
44694
+ message: null
44695
+ };
44696
+ }
44697
+ const correct = Util.deepEq(userInput.options, rubric.correct);
44698
+ return {
44699
+ type: "points",
44700
+ earned: correct ? 1 : 0,
44701
+ total: 1,
44702
+ message: null
44703
+ };
44704
+ }
44705
+
44546
44706
  const {
44547
44707
  shuffle
44548
44708
  } = Util;
@@ -44556,14 +44716,8 @@ class Sorter extends React__namespace.Component {
44556
44716
  onChange: function () {},
44557
44717
  linterContext: PerseusLinter.linterContextDefault
44558
44718
  };
44559
- static validate(state, rubric) {
44560
- const correct = ___default["default"].isEqual(state.options, rubric.correct);
44561
- return {
44562
- type: "points",
44563
- earned: correct ? 1 : 0,
44564
- total: 1,
44565
- message: null
44566
- };
44719
+ static validate(userInput, rubric) {
44720
+ return sorterValidator(userInput, rubric);
44567
44721
  }
44568
44722
  state = {
44569
44723
  changed: false
@@ -44593,33 +44747,23 @@ class Sorter extends React__namespace.Component {
44593
44747
  }, 0);
44594
44748
  });
44595
44749
  };
44596
- getUserInput = () => {
44750
+ getUserInput() {
44597
44751
  // eslint-disable-next-line react/no-string-refs
44598
44752
  // @ts-expect-error - TS2339 - Property 'getOptions' does not exist on type 'ReactInstance'.
44753
+ const options = this.refs.sortable.getOptions();
44599
44754
  return {
44600
- options: this.refs.sortable.getOptions()
44755
+ options,
44756
+ changed: this.state.changed
44601
44757
  };
44602
- };
44758
+ }
44603
44759
  moveOptionToIndex = (option, index) => {
44604
44760
  // eslint-disable-next-line react/no-string-refs
44605
44761
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
44606
44762
  this.refs.sortable.moveOptionToIndex(option, index);
44607
44763
  };
44608
- simpleValidate = rubric => {
44609
- // If this widget hasn't been changed yet, we treat it as "empty" which
44610
- // prevents the "Check" button from becoming active. We want the user
44611
- // to make a change before trying to move forward. This makes an
44612
- // assumption that the initial order isn't the correct order! However,
44613
- // this should be rare if it happens, and interacting with the list
44614
- // will enable the button, so they won't be locked out of progressing.
44615
- if (!this.state.changed) {
44616
- return {
44617
- type: "invalid",
44618
- message: null
44619
- };
44620
- }
44621
- return Sorter.validate(this.getUserInput(), rubric);
44622
- };
44764
+ simpleValidate(rubric) {
44765
+ return sorterValidator(this.getUserInput(), rubric);
44766
+ }
44623
44767
  render() {
44624
44768
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
44625
44769
  const {
@@ -44648,6 +44792,66 @@ var Sorter$1 = {
44648
44792
  isLintable: true
44649
44793
  };
44650
44794
 
44795
+ function tableValidator(state, rubric, strings) {
44796
+ const filterNonEmpty = function (table) {
44797
+ return ___default["default"].filter(table, function (row) {
44798
+ // Check if row has a cell that is nonempty
44799
+ return ___default["default"].some(row, ___default["default"].identity);
44800
+ });
44801
+ };
44802
+ const solution = filterNonEmpty(rubric.answers);
44803
+ const supplied = filterNonEmpty(state);
44804
+ const hasEmptyCell = ___default["default"].some(supplied, function (row) {
44805
+ return ___default["default"].some(row, function (cell) {
44806
+ return cell === "";
44807
+ });
44808
+ });
44809
+ if (hasEmptyCell || !supplied.length) {
44810
+ return {
44811
+ type: "invalid",
44812
+ message: null
44813
+ };
44814
+ }
44815
+ if (supplied.length !== solution.length) {
44816
+ return {
44817
+ type: "points",
44818
+ earned: 0,
44819
+ total: 1,
44820
+ message: null
44821
+ };
44822
+ }
44823
+ const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44824
+ let message = null;
44825
+ const allCorrect = ___default["default"].every(solution, function (rowSolution) {
44826
+ for (let i = 0; i < supplied.length; i++) {
44827
+ const rowSupplied = supplied[i];
44828
+ const correct = ___default["default"].every(rowSupplied, function (cellSupplied, i) {
44829
+ const cellSolution = rowSolution[i];
44830
+ const validator = createValidator(cellSolution, {
44831
+ simplify: true
44832
+ }, strings);
44833
+ const result = validator(cellSupplied);
44834
+ if (result.message) {
44835
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
44836
+ message = result.message;
44837
+ }
44838
+ return result.correct;
44839
+ });
44840
+ if (correct) {
44841
+ supplied.splice(i, 1);
44842
+ return true;
44843
+ }
44844
+ }
44845
+ return false;
44846
+ });
44847
+ return {
44848
+ type: "points",
44849
+ earned: allCorrect ? 1 : 0,
44850
+ total: 1,
44851
+ message: message
44852
+ };
44853
+ }
44854
+
44651
44855
  const {
44652
44856
  assert
44653
44857
  } = InteractiveUtil;
@@ -44693,15 +44897,18 @@ class Table extends React__namespace.Component {
44693
44897
  linterContext: PerseusLinter.linterContextDefault
44694
44898
  };
44695
44899
  }();
44900
+ static validate(state, rubric, strings) {
44901
+ return tableValidator(state, rubric, strings);
44902
+ }
44696
44903
  _getRows = () => {
44697
44904
  return this.props.answers.length;
44698
44905
  };
44699
44906
  _getColumns = () => {
44700
44907
  return this.props.answers[0].length;
44701
44908
  };
44702
- getUserInput = () => {
44909
+ getUserInput() {
44703
44910
  return ___default["default"].map(this.props.answers, ___default["default"].clone);
44704
- };
44911
+ }
44705
44912
  onValueChange = (row, column, eventOrValue) => {
44706
44913
  const answers = ___default["default"].map(this.props.answers, ___default["default"].clone);
44707
44914
 
@@ -44721,10 +44928,9 @@ class Table extends React__namespace.Component {
44721
44928
  headers: headers
44722
44929
  });
44723
44930
  };
44724
- simpleValidate = rubric => {
44725
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
44726
- return Table.validate(this.getUserInput(), rubric, this.context.strings);
44727
- };
44931
+ simpleValidate(rubric) {
44932
+ return tableValidator(this.getUserInput(), rubric, this.context.strings);
44933
+ }
44728
44934
  _handleFocus = inputPath => {
44729
44935
  this.props.onFocus(inputPath);
44730
44936
  };
@@ -44850,68 +45056,6 @@ class Table extends React__namespace.Component {
44850
45056
  })));
44851
45057
  }
44852
45058
  }
44853
- ___default["default"].extend(Table, {
44854
- validate: function (state, rubric, strings) {
44855
- const filterNonEmpty = function (table) {
44856
- return ___default["default"].filter(table, function (row) {
44857
- // Check if row has a cell that is nonempty
44858
- return ___default["default"].some(row, ___default["default"].identity);
44859
- });
44860
- };
44861
- const solution = filterNonEmpty(rubric.answers);
44862
- const supplied = filterNonEmpty(state);
44863
- const hasEmptyCell = ___default["default"].some(supplied, function (row) {
44864
- return ___default["default"].some(row, function (cell) {
44865
- return cell === "";
44866
- });
44867
- });
44868
- if (hasEmptyCell || !supplied.length) {
44869
- return {
44870
- type: "invalid",
44871
- message: null
44872
- };
44873
- }
44874
- if (supplied.length !== solution.length) {
44875
- return {
44876
- type: "points",
44877
- earned: 0,
44878
- total: 1,
44879
- message: null
44880
- };
44881
- }
44882
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44883
- let message = null;
44884
- const allCorrect = ___default["default"].every(solution, function (rowSolution) {
44885
- let i;
44886
- for (i = 0; i < supplied.length; i++) {
44887
- const rowSupplied = supplied[i];
44888
- const correct = ___default["default"].every(rowSupplied, function (cellSupplied, i) {
44889
- const cellSolution = rowSolution[i];
44890
- const validator = createValidator(cellSolution, {
44891
- simplify: true
44892
- }, strings);
44893
- const result = validator(cellSupplied);
44894
- if (result.message) {
44895
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
44896
- message = result.message;
44897
- }
44898
- return result.correct;
44899
- });
44900
- if (correct) {
44901
- supplied.splice(i, 1);
44902
- return true;
44903
- }
44904
- }
44905
- return false;
44906
- });
44907
- return {
44908
- type: "points",
44909
- earned: allCorrect ? 1 : 0,
44910
- total: 1,
44911
- message: message
44912
- };
44913
- }
44914
- });
44915
45059
  const propTransform = editorProps => {
44916
45060
  // Remove answers before passing to widget
44917
45061
  const rows = editorProps.answers.length;
@@ -45144,7 +45288,7 @@ const init = function (options) {
45144
45288
 
45145
45289
  // This file is processed by a Rollup plugin (replace) to inject the production
45146
45290
  const libName = "@khanacademy/perseus";
45147
- const libVersion = "34.1.0";
45291
+ const libVersion = "35.1.1";
45148
45292
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45149
45293
 
45150
45294
  const apiVersion = {
@@ -45698,9 +45842,6 @@ const defaultContext = {
45698
45842
  };
45699
45843
  const context = /*#__PURE__*/React__namespace.createContext(defaultContext);
45700
45844
 
45701
- const {
45702
- mapObject
45703
- } = objective_;
45704
45845
  class ServerItemRenderer extends React__namespace.Component {
45705
45846
  static contextType = PerseusI18nContext;
45706
45847
  // @ts-expect-error - TS2564 - Property 'questionRenderer' has no initializer and is not definitely assigned in the constructor.
@@ -45916,20 +46057,6 @@ class ServerItemRenderer extends React__namespace.Component {
45916
46057
  return this.questionRenderer.getWidgetIds();
45917
46058
  }
45918
46059
 
45919
- /**
45920
- * Returns an object mapping from widget ID to KE-style score.
45921
- * The keys of this object are the values of the array returned
45922
- * from `getWidgetIds`.
45923
- */
45924
- scoreWidgets() {
45925
- const qScore = this.questionRenderer.scoreWidgets();
45926
- const qGuess = this.questionRenderer.getUserInputForWidgets();
45927
- const state = this.questionRenderer.getSerializedState();
45928
- return mapObject(qScore, (score, id) => {
45929
- return Util.keScoreFromPerseusScore(score, qGuess[id], state);
45930
- });
45931
- }
45932
-
45933
46060
  /**
45934
46061
  * Get a representation of the current state of the item.
45935
46062
  */