@khanacademy/perseus 34.0.0 → 35.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/es/index.js +641 -525
  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 +573 -447
  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 +4 -5
  10. package/dist/perseus-types.d.ts +6 -18
  11. package/dist/renderer.d.ts +4 -4
  12. package/dist/server-item-renderer.d.ts +4 -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 +1 -3
  17. package/dist/validation.types.d.ts +111 -0
  18. package/dist/widgets/__shared__/noop-validator.d.ts +2 -2
  19. package/dist/widgets/categorizer/categorizer-validator.d.ts +5 -0
  20. package/dist/widgets/categorizer/categorizer.d.ts +6 -9
  21. package/dist/widgets/cs-program/cs-program.d.ts +4 -9
  22. package/dist/widgets/definition/definition.d.ts +6 -7
  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 +6 -7
  26. package/dist/widgets/expression/expression-validator.d.ts +2 -2
  27. package/dist/widgets/expression/expression.d.ts +38 -26
  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.d.ts +6 -7
  32. package/dist/widgets/group/group.d.ts +3 -2
  33. package/dist/widgets/iframe/iframe.d.ts +4 -9
  34. package/dist/widgets/image/image.d.ts +6 -7
  35. package/dist/widgets/input-number/input-number-validator.d.ts +41 -0
  36. package/dist/widgets/input-number/input-number.d.ts +7 -10
  37. package/dist/widgets/interaction/interaction.d.ts +6 -6
  38. package/dist/widgets/interactive-graph.d.ts +9 -12
  39. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +15 -7
  40. package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +16 -1
  41. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +5 -2
  42. package/dist/widgets/interactive-graphs/types.d.ts +3 -0
  43. package/dist/widgets/label-image/label-image-validator.d.ts +10 -0
  44. package/dist/widgets/label-image/label-image.d.ts +13 -10
  45. package/dist/widgets/label-image/types.d.ts +0 -4
  46. package/dist/widgets/matcher/matcher.d.ts +4 -4
  47. package/dist/widgets/matrix/matrix.d.ts +5 -5
  48. package/dist/widgets/molecule/molecule.d.ts +4 -3
  49. package/dist/widgets/number-line/number-line.d.ts +5 -3
  50. package/dist/widgets/numeric-input/numeric-input-validator.d.ts +2 -2
  51. package/dist/widgets/numeric-input/numeric-input.d.ts +7 -7
  52. package/dist/widgets/orderer/orderer.d.ts +4 -4
  53. package/dist/widgets/passage/passage.d.ts +6 -7
  54. package/dist/widgets/passage-ref/passage-ref.d.ts +6 -14
  55. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +4 -3
  56. package/dist/widgets/phet-simulation/phet-simulation.d.ts +2 -2
  57. package/dist/widgets/plotter/plotter.d.ts +5 -5
  58. package/dist/widgets/radio/radio-component.d.ts +6 -13
  59. package/dist/widgets/radio/radio-validator.d.ts +5 -0
  60. package/dist/widgets/sorter/sorter-validator.d.ts +4 -0
  61. package/dist/widgets/sorter/sorter.d.ts +7 -9
  62. package/dist/widgets/table/table.d.ts +5 -5
  63. package/dist/widgets/video/video.d.ts +5 -6
  64. package/package.json +1 -1
  65. package/dist/widgets/dropdown/dropdown.types.d.ts +0 -5
  66. package/dist/widgets/numeric-input/numeric-input.types.d.ts +0 -5
package/dist/es/index.js CHANGED
@@ -246,7 +246,10 @@ const mockStrings = {
246
246
  cos: "cos",
247
247
  tan: "tan",
248
248
  simulationLoadFail: "Sorry, this simulation cannot load.",
249
- simulationLocaleWarning: "Sorry, this simulation isn't available in your language."
249
+ simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
250
+ addPoint: "Add Point",
251
+ removePoint: "Remove Point",
252
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph"
250
253
  };
251
254
 
252
255
  /**
@@ -749,7 +752,7 @@ const denylist = [
749
752
  // (technically the renderer still adds them)
750
753
  "key", "ref",
751
754
  // added by src/renderer.jsx
752
- "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "questionCompleted", "findWidgets",
755
+ "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "widgetIsOpen", "questionCompleted", "findWidgets",
753
756
  // added by src/editor.jsx, for widgets removing themselves
754
757
  // this is soooo not the right place for this, but alas.
755
758
  "onRemove",
@@ -757,15 +760,6 @@ const denylist = [
757
760
  "id",
758
761
  // Callbacks and items for interaction handling
759
762
  "onBlur", "onFocus", "trackInteraction", "keypadElement"];
760
- const removeDenylistProps = props => {
761
- const newProps = _extends({}, props);
762
- for (const prop of denylist) {
763
- if (prop in newProps) {
764
- delete newProps[prop];
765
- }
766
- }
767
- return newProps;
768
- };
769
763
 
770
764
  /* eslint-disable @babel/no-invalid-this */
771
765
  const USAGE = "Usage:\n" + " this.change({propName: 5}, callback);\n" + ' this.change("propName", 5, callback);\n' + ' this.change("propName")';
@@ -1030,8 +1024,6 @@ StubTagEditor.defaultProps = {
1030
1024
  * * nothing if it is purely a bug fix.
1031
1025
  *
1032
1026
  * Callbacks passed to Renderer/ItemRenderer:
1033
- * * onInputError:
1034
- * Called when there is an error grading a widget
1035
1027
  * * onFocusChange: (newFocusPath, oldFocusPath, keypadDOMNode)
1036
1028
  * Called when the user focus changes. The first two parameters are `path`
1037
1029
  * arrays uniquely identifying the respect inputs. The third parameter,
@@ -1054,7 +1046,6 @@ StubTagEditor.defaultProps = {
1054
1046
  const ApiOptions = {
1055
1047
  propTypes: PropTypes.shape({
1056
1048
  isArticle: PropTypes.bool.isRequired,
1057
- onInputError: PropTypes.func.isRequired,
1058
1049
  onFocusChange: PropTypes.func.isRequired,
1059
1050
  GroupMetadataEditor: PropTypes.func.isRequired,
1060
1051
  showAlignmentOptions: PropTypes.bool.isRequired,
@@ -1138,7 +1129,6 @@ const ApiOptions = {
1138
1129
  defaults: {
1139
1130
  isArticle: false,
1140
1131
  isMobile: false,
1141
- onInputError: function () {},
1142
1132
  onFocusChange: function () {},
1143
1133
  GroupMetadataEditor: StubTagEditor,
1144
1134
  showAlignmentOptions: false,
@@ -2150,9 +2140,7 @@ const getDecimalSeparator = locale => {
2150
2140
  * show the user an error. TODO(joel) - what error?
2151
2141
  * - Otherwise, pass through the resulting points and message.
2152
2142
  */
2153
- function expressionValidator(userInput, rubric,
2154
- // @ts-expect-error - TS2322 - Type '() => void' is not assignable to type 'OnInputErrorFunctionType'.
2155
- onInputError = function () {}, strings, locale) {
2143
+ function expressionValidator(userInput, rubric, strings, locale) {
2156
2144
  const options = _.clone(rubric);
2157
2145
  _.extend(options, {
2158
2146
  decimal_separator: getDecimalSeparator(locale)
@@ -2252,14 +2240,9 @@ onInputError = function () {}, strings, locale) {
2252
2240
  };
2253
2241
  }
2254
2242
  if (matchingAnswerForm.considered === "ungraded") {
2255
- // We matched an ungraded answer form - return "invalid", which
2256
- // will let the user try again with no penalty
2257
- const apiResult = onInputError(null,
2258
- // Reserved for some widget identifier
2259
- userInput, matchMessage);
2260
2243
  return {
2261
2244
  type: "invalid",
2262
- message: apiResult === false ? null : matchMessage
2245
+ message: matchMessage
2263
2246
  };
2264
2247
  }
2265
2248
  // We matched a graded answer form, so we can now tell the user
@@ -2348,14 +2331,9 @@ class Expression extends React.Component {
2348
2331
  showErrorStyle: false
2349
2332
  });
2350
2333
  if (!this.parse(this.props.value, this.props).parsed) {
2351
- const apiResult = this.props.apiOptions.onInputError(null,
2352
- // reserved for some widget identifier
2353
- this.props.value, this.context.strings.ERROR_TITLE);
2354
- if (apiResult !== false) {
2355
- this.setState({
2356
- invalid: true
2357
- });
2358
- }
2334
+ this.setState({
2335
+ invalid: true
2336
+ });
2359
2337
  }
2360
2338
  }
2361
2339
  };
@@ -2369,12 +2347,12 @@ class Expression extends React.Component {
2369
2347
  });
2370
2348
  }
2371
2349
  };
2372
- this.simpleValidate = (_ref, onInputError) => {
2350
+ this.simpleValidate = _ref => {
2373
2351
  let {
2374
2352
  scoring
2375
2353
  } = _ref,
2376
2354
  rubric = _objectWithoutPropertiesLoose(_ref, _excluded$5);
2377
- const score = expressionValidator(this.getUserInput(), rubric, onInputError || function () {}, this.context.strings, this.context.locale);
2355
+ const score = expressionValidator(this.getUserInput(), rubric, this.context.strings, this.context.locale);
2378
2356
 
2379
2357
  // "scoring" is a flag that indicates when we are checking answers.
2380
2358
  // otherwise, we may just be checking validity after changes.
@@ -2390,9 +2368,6 @@ class Expression extends React.Component {
2390
2368
  }
2391
2369
  return score;
2392
2370
  };
2393
- this.getUserInput = () => {
2394
- return Expression.getUserInputFromProps(this.props);
2395
- };
2396
2371
  this.change = (...args) => {
2397
2372
  return change.apply(this, args);
2398
2373
  };
@@ -2465,10 +2440,8 @@ class Expression extends React.Component {
2465
2440
  };
2466
2441
  }
2467
2442
  // TODO remove this in favor of just using expressionValidator
2468
- static validate(userInput, rubric,
2469
- // @ts-expect-error - TS2322 - Type '() => void' is not assignable to type 'OnInputErrorFunctionType'.
2470
- onInputError = function () {}, strings, locale) {
2471
- return expressionValidator(userInput, rubric, onInputError, strings, locale);
2443
+ static validate(userInput, rubric, strings, locale) {
2444
+ return expressionValidator(userInput, rubric, strings, locale);
2472
2445
  }
2473
2446
  static getUserInputFromProps(props) {
2474
2447
  return normalizeTex(props.value);
@@ -2482,6 +2455,9 @@ class Expression extends React.Component {
2482
2455
  }
2483
2456
  //#endregion
2484
2457
 
2458
+ getUserInput() {
2459
+ return Expression.getUserInputFromProps(this.props);
2460
+ }
2485
2461
  // HACK(joel)
2486
2462
  insert(keyPressed) {
2487
2463
  // eslint-disable-next-line react/no-string-refs
@@ -14677,7 +14653,7 @@ const pluck = function pluck(table, subKey) {
14677
14653
  * });
14678
14654
  * {a: 2, b: 3}
14679
14655
  */
14680
- const mapObject$2 = function mapObject(obj, lambda) {
14656
+ const mapObject$1 = function mapObject(obj, lambda) {
14681
14657
  const result = {};
14682
14658
  _.each(_.keys(obj), function (key) {
14683
14659
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14703,7 +14679,7 @@ const mapObjectFromArray$1 = function mapObjectFromArray(arr, lambda) {
14703
14679
  };
14704
14680
  var objective_ = {
14705
14681
  pluck: pluck,
14706
- mapObject: mapObject$2,
14682
+ mapObject: mapObject$1,
14707
14683
  mapObjectFromArray: mapObjectFromArray$1
14708
14684
  };
14709
14685
 
@@ -19596,7 +19572,7 @@ WidgetContainer.defaultProps = {
19596
19572
  };
19597
19573
 
19598
19574
  const {
19599
- mapObject: mapObject$1,
19575
+ mapObject,
19600
19576
  mapObjectFromArray
19601
19577
  } = objective_;
19602
19578
  const rContainsNonWhitespace = /\S/;
@@ -19624,9 +19600,6 @@ const isIdPathPrefix = function isIdPathPrefix(prefixArray, wholeArray) {
19624
19600
  });
19625
19601
  };
19626
19602
 
19627
- // The return type for getUserInput. Widgets have full control of what is
19628
- // returned so it's not easily typed (some widgets return a scalar (string),
19629
- // some return a custom-built object
19630
19603
  /**
19631
19604
  * The Widget type represents the common API that the Renderer uses to interact
19632
19605
  * with all widgets. All widgets must implement the methods in this API, unless
@@ -19636,6 +19609,7 @@ const isIdPathPrefix = function isIdPathPrefix(prefixArray, wholeArray) {
19636
19609
  * communicate with the individual widgets to coordinate actions such as
19637
19610
  * scoring, state serialization/deserialization, and focus management.
19638
19611
  */
19612
+
19639
19613
  class Renderer extends React.Component {
19640
19614
  constructor(_props) {
19641
19615
  super(_props);
@@ -19670,7 +19644,7 @@ class Renderer extends React.Component {
19670
19644
  };
19671
19645
  // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19672
19646
  this._getAllWidgetsInfo = props => {
19673
- return mapObject$1(props.widgets, (widgetInfo, widgetId) => {
19647
+ return mapObject(props.widgets, (widgetInfo, widgetId) => {
19674
19648
  if (!widgetInfo.type || !widgetInfo.alignment) {
19675
19649
  const newValues = {};
19676
19650
  if (!widgetInfo.type) {
@@ -19811,7 +19785,7 @@ class Renderer extends React.Component {
19811
19785
  * state from that instead of the current widget props.
19812
19786
  */
19813
19787
  this.getSerializedState = widgetProps => {
19814
- return mapObject$1(widgetProps || this.state.widgetProps, (props, widgetId) => {
19788
+ return mapObject(widgetProps || this.state.widgetProps, (props, widgetId) => {
19815
19789
  const widget = this.getWidgetInstance(widgetId);
19816
19790
  if (widget && widget.getSerializedState) {
19817
19791
  return widget.getSerializedState();
@@ -19847,7 +19821,7 @@ class Renderer extends React.Component {
19847
19821
  }
19848
19822
  };
19849
19823
  this.setState({
19850
- widgetProps: mapObject$1(serializedState, (props, widgetId) => {
19824
+ widgetProps: mapObject(serializedState, (props, widgetId) => {
19851
19825
  const widget = this.getWidgetInstance(widgetId);
19852
19826
  if (widget && widget.restoreSerializedState) {
19853
19827
  // Note that we probably can't call
@@ -20681,7 +20655,6 @@ class Renderer extends React.Component {
20681
20655
  */
20682
20656
  this.scoreWidgets = () => {
20683
20657
  const widgetProps = this.state.widgetInfo;
20684
- const onInputError = this.getApiOptions().onInputError;
20685
20658
  const gradedWidgetIds = _.filter(this.widgetIds, id => {
20686
20659
  const props = widgetProps[id];
20687
20660
  const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
@@ -20697,7 +20670,7 @@ class Renderer extends React.Component {
20697
20670
  if (widget && widget.simpleValidate) {
20698
20671
  widgetScores[id] = widget.simpleValidate(_extends({}, props == null ? void 0 : props.options, {
20699
20672
  scoring: true
20700
- }), onInputError);
20673
+ }));
20701
20674
  }
20702
20675
  });
20703
20676
  return widgetScores;
@@ -21738,6 +21711,43 @@ const answerTypes = {
21738
21711
  forms: "pi"
21739
21712
  }
21740
21713
  };
21714
+ function inputNumberValidator(state, rubric, strings) {
21715
+ if (rubric.answerType == null) {
21716
+ rubric.answerType = "number";
21717
+ }
21718
+
21719
+ // note(matthewc): this will get immediately parsed again by
21720
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21721
+ // expected here
21722
+ const stringValue = `${rubric.value}`;
21723
+ const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21724
+ simplify: rubric.simplify,
21725
+ inexact: rubric.inexact || undefined,
21726
+ maxError: rubric.maxError,
21727
+ forms: answerTypes[rubric.answerType].forms
21728
+ }, strings);
21729
+
21730
+ // We may have received TeX; try to parse it before grading.
21731
+ // If `currentValue` is not TeX, this should be a no-op.
21732
+ const currentValue = ParseTex$1(state.currentValue);
21733
+ const result = val(currentValue);
21734
+
21735
+ // TODO(eater): Seems silly to translate result to this invalid/points
21736
+ // thing and immediately translate it back in ItemRenderer.scoreInput()
21737
+ if (result.empty) {
21738
+ return {
21739
+ type: "invalid",
21740
+ message: result.message
21741
+ };
21742
+ }
21743
+ return {
21744
+ type: "points",
21745
+ earned: result.correct ? 1 : 0,
21746
+ total: 1,
21747
+ message: result.message
21748
+ };
21749
+ }
21750
+
21741
21751
  const formExamples$1 = {
21742
21752
  integer: function (options, strings) {
21743
21753
  return strings.integerExample;
@@ -21823,13 +21833,6 @@ class InputNumber extends React.Component {
21823
21833
  currentValue: newValue
21824
21834
  }, cb);
21825
21835
  };
21826
- this.getUserInput = () => {
21827
- return InputNumber.getUserInputFromProps(this.props);
21828
- };
21829
- this.simpleValidate = (rubric, onInputError) => {
21830
- onInputError = onInputError || function () {};
21831
- return InputNumber.validate(this.getUserInput(), rubric, this.context.strings, onInputError);
21832
- };
21833
21836
  this.examples = () => {
21834
21837
  const {
21835
21838
  strings
@@ -21840,45 +21843,8 @@ class InputNumber extends React.Component {
21840
21843
  return [strings.yourAnswer].concat(examples);
21841
21844
  };
21842
21845
  }
21843
- static validate(state, rubric, strings, onInputError = () => {}) {
21844
- if (rubric.answerType == null) {
21845
- rubric.answerType = "number";
21846
- }
21847
-
21848
- // note(matthewc): this will get immediately parsed again by
21849
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21850
- // expected here
21851
- const stringValue = `${rubric.value}`;
21852
- const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21853
- simplify: rubric.simplify,
21854
- inexact: rubric.inexact || undefined,
21855
- maxError: rubric.maxError,
21856
- forms: answerTypes[rubric.answerType].forms
21857
- }, strings);
21858
-
21859
- // We may have received TeX; try to parse it before grading.
21860
- // If `currentValue` is not TeX, this should be a no-op.
21861
- const currentValue = ParseTex$1(state.currentValue);
21862
- const result = val(currentValue);
21863
-
21864
- // TODO(eater): Seems silly to translate result to this invalid/points
21865
- // thing and immediately translate it back in ItemRenderer.scoreInput()
21866
- if (result.empty) {
21867
- // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
21868
- const apiResult = onInputError == null ? void 0 : onInputError(null,
21869
- // reserved for some widget identifier
21870
- state.currentValue, result.message);
21871
- return {
21872
- type: "invalid",
21873
- message: apiResult === false ? null : result.message
21874
- };
21875
- }
21876
- return {
21877
- type: "points",
21878
- earned: result.correct ? 1 : 0,
21879
- total: 1,
21880
- message: result.message
21881
- };
21846
+ static validate(state, rubric, strings) {
21847
+ return inputNumberValidator(state, rubric, strings);
21882
21848
  }
21883
21849
  static getUserInputFromProps(props) {
21884
21850
  return {
@@ -21895,6 +21861,12 @@ class InputNumber extends React.Component {
21895
21861
  }
21896
21862
  return answerString;
21897
21863
  }
21864
+ getUserInput() {
21865
+ return InputNumber.getUserInputFromProps(this.props);
21866
+ }
21867
+ simpleValidate(rubric) {
21868
+ return inputNumberValidator(this.getUserInput(), rubric, this.context.strings);
21869
+ }
21898
21870
  render() {
21899
21871
  if (this.props.apiOptions.customKeypad) {
21900
21872
  // TODO(charlie): Support "Review Mode".
@@ -22198,9 +22170,6 @@ class NumericInput extends React.Component {
22198
22170
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22199
22171
  return !noFormsAccepted && !allFormsAccepted;
22200
22172
  };
22201
- this.simpleValidate = rubric => {
22202
- return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22203
- };
22204
22173
  this.focus = () => {
22205
22174
  var _this$inputRef;
22206
22175
  (_this$inputRef = this.inputRef) == null || _this$inputRef.focus();
@@ -22230,9 +22199,6 @@ class NumericInput extends React.Component {
22230
22199
  currentValue: newValue
22231
22200
  }, cb);
22232
22201
  };
22233
- this.getUserInput = () => {
22234
- return NumericInput.getUserInputFromProps(this.props);
22235
- };
22236
22202
  this.handleChange = (newValue, cb) => {
22237
22203
  this.props.onChange({
22238
22204
  currentValue: newValue
@@ -22282,6 +22248,12 @@ class NumericInput extends React.Component {
22282
22248
  static validate(userInput, rubric, strings) {
22283
22249
  return numericInputValidator(userInput, rubric, strings);
22284
22250
  }
22251
+ simpleValidate(rubric) {
22252
+ return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22253
+ }
22254
+ getUserInput() {
22255
+ return NumericInput.getUserInputFromProps(this.props);
22256
+ }
22285
22257
  render() {
22286
22258
  let labelText = this.props.labelText;
22287
22259
  if (labelText == null || labelText === "") {
@@ -22412,15 +22384,15 @@ var NumericInput$1 = {
22412
22384
  * Several widgets don't have "right"/"wrong" validation logic,
22413
22385
  * so this just says to move on past those widgets
22414
22386
  *
22415
- * TODO: widgets that use this probably shouldn't have any
22387
+ * TODO(LEMS-2396) widgets that use this probably shouldn't have any
22416
22388
  * validation logic and the thing validating an exercise
22417
22389
  * should just know to skip these
22418
22390
  */
22419
- function noopValidator() {
22391
+ function noopValidator(points = 0) {
22420
22392
  return {
22421
22393
  type: "points",
22422
- earned: 0,
22423
- total: 0,
22394
+ earned: points,
22395
+ total: points,
22424
22396
  message: null
22425
22397
  };
22426
22398
  }
@@ -22438,11 +22410,6 @@ class PassageRef extends React.Component {
22438
22410
  lineRange: null,
22439
22411
  content: null
22440
22412
  };
22441
- // TODO(tamarab): getUserInput needs to be updated to only return
22442
- // props input by the user. Currently returns all the widget's props.
22443
- this.getUserInput = () => {
22444
- return removeDenylistProps(this.props);
22445
- };
22446
22413
  this.change = (...args) => {
22447
22414
  return change.apply(this, args);
22448
22415
  };
@@ -22472,11 +22439,9 @@ class PassageRef extends React.Component {
22472
22439
  }
22473
22440
  }
22474
22441
  };
22475
- this.simpleValidate = rubric => {
22476
- return PassageRef.validate(this.getUserInput(), rubric);
22477
- };
22478
22442
  }
22479
- static validate(userInput, rubric) {
22443
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22444
+ static validate() {
22480
22445
  return noopValidator();
22481
22446
  }
22482
22447
  componentDidMount() {
@@ -22497,6 +22462,15 @@ class PassageRef extends React.Component {
22497
22462
  window.removeEventListener("resize", this._throttledUpdateRange);
22498
22463
  this._isMounted = false;
22499
22464
  }
22465
+
22466
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22467
+ getUserInput() {
22468
+ return null;
22469
+ }
22470
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22471
+ simpleValidate() {
22472
+ return noopValidator();
22473
+ }
22500
22474
  render() {
22501
22475
  const {
22502
22476
  strings
@@ -23898,6 +23872,56 @@ const styles$t = StyleSheet.create({
23898
23872
  }
23899
23873
  });
23900
23874
 
23875
+ function radioValidator(userInput, rubric, strings) {
23876
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
23877
+ return sum + (selected ? 1 : 0);
23878
+ }, 0);
23879
+ if (numSelected === 0) {
23880
+ return {
23881
+ type: "invalid",
23882
+ message: null
23883
+ };
23884
+ }
23885
+
23886
+ // TODO: should numCorrect actually be on the rubric
23887
+ // instead of the userInput?
23888
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
23889
+ return {
23890
+ type: "invalid",
23891
+ message: strings.chooseCorrectNum
23892
+ };
23893
+ // If NOTA and some other answer are checked, ...
23894
+ }
23895
+
23896
+ // TODO: should noneOfTheAboveSelected be replaced with a
23897
+ // combination of choicesSelected and noneOfTheAboveIndex?
23898
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
23899
+ return {
23900
+ type: "invalid",
23901
+ message: strings.notNoneOfTheAbove
23902
+ };
23903
+ }
23904
+ const correct = userInput.choicesSelected.every((selected, i) => {
23905
+ let isCorrect;
23906
+ // TODO: should noneOfTheAboveIndex actually be on the rubric
23907
+ // instead of the userInput?
23908
+ if (userInput.noneOfTheAboveIndex === i) {
23909
+ isCorrect = rubric.choices.every((choice, j) => {
23910
+ return i === j || !choice.correct;
23911
+ });
23912
+ } else {
23913
+ isCorrect = !!rubric.choices[i].correct;
23914
+ }
23915
+ return isCorrect === selected;
23916
+ });
23917
+ return {
23918
+ type: "points",
23919
+ earned: correct ? 1 : 0,
23920
+ total: 1,
23921
+ message: null
23922
+ };
23923
+ }
23924
+
23901
23925
  // RenderProps is the return type for radio.jsx#transform
23902
23926
 
23903
23927
  class Radio$1 extends React.Component {
@@ -23992,12 +24016,6 @@ class Radio$1 extends React.Component {
23992
24016
  });
23993
24017
  this.props.trackInteraction();
23994
24018
  };
23995
- this.getUserInput = () => {
23996
- return Radio$1.getUserInputFromProps(this.props);
23997
- };
23998
- this.simpleValidate = rubric => {
23999
- return Radio$1.validate(this.getUserInput(), rubric, this.context.strings);
24000
- };
24001
24019
  /**
24002
24020
  * Turn on rationale display for the currently selected choices. Note that
24003
24021
  * this leaves rationales on for choices that are already showing
@@ -24058,46 +24076,7 @@ class Radio$1 extends React.Component {
24058
24076
  };
24059
24077
  }
24060
24078
  static validate(userInput, rubric, strings) {
24061
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
24062
- return sum + (selected ? 1 : 0);
24063
- }, 0);
24064
- if (numSelected === 0) {
24065
- return {
24066
- type: "invalid",
24067
- message: null
24068
- };
24069
- }
24070
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
24071
- return {
24072
- type: "invalid",
24073
- message: strings.chooseCorrectNum
24074
- };
24075
- // If NOTA and some other answer are checked, ...
24076
- }
24077
-
24078
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
24079
- return {
24080
- type: "invalid",
24081
- message: strings.notNoneOfTheAbove
24082
- };
24083
- }
24084
- const correct = userInput.choicesSelected.every((selected, i) => {
24085
- let isCorrect;
24086
- if (userInput.noneOfTheAboveIndex === i) {
24087
- isCorrect = rubric.choices.every((choice, j) => {
24088
- return i === j || !choice.correct;
24089
- });
24090
- } else {
24091
- isCorrect = !!rubric.choices[i].correct;
24092
- }
24093
- return isCorrect === selected;
24094
- });
24095
- return {
24096
- type: "points",
24097
- earned: correct ? 1 : 0,
24098
- total: 1,
24099
- message: null
24100
- };
24079
+ return radioValidator(userInput, rubric, strings);
24101
24080
  }
24102
24081
  static getUserInputFromProps(props) {
24103
24082
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
@@ -24186,6 +24165,12 @@ class Radio$1 extends React.Component {
24186
24165
  registerFocusFunction(fun) {
24187
24166
  this.focusFunction = fun;
24188
24167
  }
24168
+ getUserInput() {
24169
+ return Radio$1.getUserInputFromProps(this.props);
24170
+ }
24171
+ simpleValidate(rubric) {
24172
+ return radioValidator(this.getUserInput(), rubric, this.context.strings);
24173
+ }
24189
24174
  render() {
24190
24175
  const {
24191
24176
  choices
@@ -24426,6 +24411,31 @@ var Radio = {
24426
24411
  /* eslint-disable import/no-named-as-default */
24427
24412
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
24428
24413
 
24414
+ function categorizerValidator(userInput, rubric, strings) {
24415
+ let completed = true;
24416
+ let allCorrect = true;
24417
+ rubric.values.forEach((value, i) => {
24418
+ if (userInput.values[i] == null) {
24419
+ completed = false;
24420
+ }
24421
+ if (userInput.values[i] !== value) {
24422
+ allCorrect = false;
24423
+ }
24424
+ });
24425
+ if (!completed) {
24426
+ return {
24427
+ type: "invalid",
24428
+ message: strings.invalidSelection
24429
+ };
24430
+ }
24431
+ return {
24432
+ type: "points",
24433
+ earned: allCorrect ? 1 : 0,
24434
+ total: 1,
24435
+ message: null
24436
+ };
24437
+ }
24438
+
24429
24439
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
24430
24440
  class Categorizer extends React.Component {
24431
24441
  constructor(..._args) {
@@ -24437,42 +24447,18 @@ class Categorizer extends React.Component {
24437
24447
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
24438
24448
  return change.apply(this, args);
24439
24449
  };
24440
- this.getUserInput = () => {
24441
- return Categorizer.getUserInputFromProps(this.props);
24442
- };
24443
- this.simpleValidate = rubric => {
24444
- return Categorizer.validate(this.getUserInput(), rubric, this.context.strings);
24445
- };
24446
24450
  }
24447
24451
  static validate(userInput, rubric, strings) {
24448
- let completed = true;
24449
- let allCorrect = true;
24450
- rubric.values.forEach((value, i) => {
24451
- if (userInput.values[i] == null) {
24452
- completed = false;
24453
- }
24454
- if (userInput.values[i] !== value) {
24455
- allCorrect = false;
24456
- }
24457
- });
24458
- if (!completed) {
24459
- return {
24460
- type: "invalid",
24461
- message: strings.invalidSelection
24462
- };
24463
- }
24464
- return {
24465
- type: "points",
24466
- earned: allCorrect ? 1 : 0,
24467
- total: 1,
24468
- message: null
24469
- };
24452
+ return categorizerValidator(userInput, rubric, strings);
24470
24453
  }
24471
24454
  static getUserInputFromProps(props) {
24472
24455
  return {
24473
24456
  values: props.values
24474
24457
  };
24475
24458
  }
24459
+ getUserInput() {
24460
+ return Categorizer.getUserInputFromProps(this.props);
24461
+ }
24476
24462
  onChange(itemNum, catNum) {
24477
24463
  const values = [...this.props.values];
24478
24464
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -24480,6 +24466,9 @@ class Categorizer extends React.Component {
24480
24466
  this.change("values", values);
24481
24467
  this.props.trackInteraction();
24482
24468
  }
24469
+ simpleValidate(rubric) {
24470
+ return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24471
+ }
24483
24472
  render() {
24484
24473
  const self = this;
24485
24474
 
@@ -24846,16 +24835,16 @@ var CSProgram$1 = {
24846
24835
  };
24847
24836
 
24848
24837
  class Definition extends React.Component {
24849
- constructor(...args) {
24850
- super(...args);
24851
- this.getUserInput = () => {
24852
- return {};
24853
- };
24854
- this.simpleValidate = rubric => {
24855
- return Definition.validate(this.getUserInput(), rubric);
24856
- };
24838
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24839
+ static validate() {
24840
+ return noopValidator();
24857
24841
  }
24858
- static validate(userInput, rubric) {
24842
+ getUserInput() {
24843
+ return {};
24844
+ }
24845
+
24846
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24847
+ simpleValidate() {
24859
24848
  return noopValidator();
24860
24849
  }
24861
24850
  render() {
@@ -24924,18 +24913,13 @@ class DeprecatedStandin extends React__default.Component {
24924
24913
  return {};
24925
24914
  };
24926
24915
  this.simpleValidate = rubric => {
24927
- return DeprecatedStandin.validate(this.getUserInput(), rubric);
24916
+ return noopValidator(1);
24928
24917
  };
24929
24918
  }
24930
24919
  static validate(userInput, rubric) {
24931
24920
  // Since this mean to replace an existing widget the learner
24932
24921
  // WILL earn points for this widget
24933
- return {
24934
- type: "points",
24935
- earned: 1,
24936
- total: 1,
24937
- message: null
24938
- };
24922
+ return noopValidator(1);
24939
24923
  }
24940
24924
  render() {
24941
24925
  return /*#__PURE__*/React__default.createElement("div", {
@@ -24995,18 +24979,18 @@ class Dropdown extends React.Component {
24995
24979
  selected: selected
24996
24980
  });
24997
24981
  };
24998
- this.getUserInput = () => {
24999
- return {
25000
- value: this.props.selected
25001
- };
25002
- };
25003
- this.simpleValidate = rubric => {
25004
- return dropdownValidator(this.getUserInput(), rubric);
25005
- };
25006
24982
  }
25007
24983
  static validate(userInput, rubric) {
25008
24984
  return dropdownValidator(userInput, rubric);
25009
24985
  }
24986
+ getUserInput() {
24987
+ return {
24988
+ value: this.props.selected
24989
+ };
24990
+ }
24991
+ simpleValidate(rubric) {
24992
+ return dropdownValidator(this.getUserInput(), rubric);
24993
+ }
25010
24994
  render() {
25011
24995
  const children = [/*#__PURE__*/React.createElement(OptionItem, {
25012
24996
  key: "placeholder",
@@ -25080,14 +25064,9 @@ class Explanation extends React.Component {
25080
25064
  });
25081
25065
  this.props.trackInteraction();
25082
25066
  };
25083
- this.getUserInput = () => {
25084
- return {};
25085
- };
25086
- this.simpleValidate = rubric => {
25087
- return Explanation.validate(this.getUserInput(), rubric);
25088
- };
25089
25067
  }
25090
- static validate(userInput, rubric) {
25068
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25069
+ static validate() {
25091
25070
  return noopValidator();
25092
25071
  }
25093
25072
  componentDidMount() {
@@ -25096,6 +25075,14 @@ class Explanation extends React.Component {
25096
25075
  componentWillUnmount() {
25097
25076
  this._mounted = false;
25098
25077
  }
25078
+ getUserInput() {
25079
+ return {};
25080
+ }
25081
+
25082
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25083
+ simpleValidate() {
25084
+ return noopValidator();
25085
+ }
25099
25086
  render() {
25100
25087
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
25101
25088
  const caretIcon = this.state.expanded ? caretUp : caretDown;
@@ -26715,7 +26702,7 @@ FunctionGrapher.defaultProps = {
26715
26702
  coords: null,
26716
26703
  asymptote: null,
26717
26704
  isMobile: false
26718
- }; // Really this is props.plot, I think
26705
+ };
26719
26706
  /* Widget and editor. */
26720
26707
  class Grapher extends React.Component {
26721
26708
  constructor(...args) {
@@ -26931,49 +26918,41 @@ class Group extends React.Component {
26931
26918
  this.change = (...args) => {
26932
26919
  return change.apply(this, args);
26933
26920
  };
26934
- this.getUserInput = () => {
26935
- var _this$rendererRef;
26936
- return (_this$rendererRef = this.rendererRef) == null ? void 0 : _this$rendererRef.getUserInput();
26937
- };
26938
26921
  this.getSerializedState = () => {
26939
- var _this$rendererRef2;
26940
- return (_this$rendererRef2 = this.rendererRef) == null ? void 0 : _this$rendererRef2.getSerializedState();
26922
+ var _this$rendererRef;
26923
+ return (_this$rendererRef = this.rendererRef) == null ? void 0 : _this$rendererRef.getSerializedState();
26941
26924
  };
26942
26925
  this.restoreSerializedState = (state, callback) => {
26943
- var _this$rendererRef3;
26944
- (_this$rendererRef3 = this.rendererRef) == null || _this$rendererRef3.restoreSerializedState(state, callback);
26926
+ var _this$rendererRef2;
26927
+ (_this$rendererRef2 = this.rendererRef) == null || _this$rendererRef2.restoreSerializedState(state, callback);
26945
26928
  // Tell our renderer that we have no props to change
26946
26929
  // (all our changes were in state):
26947
26930
  return null;
26948
26931
  };
26949
- this.simpleValidate = rubric => {
26950
- var _this$rendererRef4;
26951
- return (_this$rendererRef4 = this.rendererRef) == null ? void 0 : _this$rendererRef4.score();
26952
- };
26953
26932
  // Mobile API:
26954
26933
  this.getInputPaths = () => {
26955
- var _this$rendererRef5;
26956
- return (_this$rendererRef5 = this.rendererRef) == null ? void 0 : _this$rendererRef5.getInputPaths();
26934
+ var _this$rendererRef3;
26935
+ return (_this$rendererRef3 = this.rendererRef) == null ? void 0 : _this$rendererRef3.getInputPaths();
26957
26936
  };
26958
26937
  this.setInputValue = (path, newValue, callback) => {
26959
- var _this$rendererRef6;
26960
- return (_this$rendererRef6 = this.rendererRef) == null ? void 0 : _this$rendererRef6.setInputValue(path, newValue, callback);
26938
+ var _this$rendererRef4;
26939
+ return (_this$rendererRef4 = this.rendererRef) == null ? void 0 : _this$rendererRef4.setInputValue(path, newValue, callback);
26961
26940
  };
26962
26941
  this.focus = () => {
26963
- var _this$rendererRef7;
26964
- return (_this$rendererRef7 = this.rendererRef) == null ? void 0 : _this$rendererRef7.focus();
26942
+ var _this$rendererRef5;
26943
+ return (_this$rendererRef5 = this.rendererRef) == null ? void 0 : _this$rendererRef5.focus();
26965
26944
  };
26966
26945
  this.focusInputPath = path => {
26967
- var _this$rendererRef8;
26968
- (_this$rendererRef8 = this.rendererRef) == null || _this$rendererRef8.focusPath(path);
26946
+ var _this$rendererRef6;
26947
+ (_this$rendererRef6 = this.rendererRef) == null || _this$rendererRef6.focusPath(path);
26969
26948
  };
26970
26949
  this.blurInputPath = path => {
26971
- var _this$rendererRef9;
26972
- (_this$rendererRef9 = this.rendererRef) == null || _this$rendererRef9.blurPath(path);
26950
+ var _this$rendererRef7;
26951
+ (_this$rendererRef7 = this.rendererRef) == null || _this$rendererRef7.blurPath(path);
26973
26952
  };
26974
26953
  this.showRationalesForCurrentlySelectedChoices = () => {
26975
- var _this$rendererRef10;
26976
- (_this$rendererRef10 = this.rendererRef) == null || _this$rendererRef10.showRationalesForCurrentlySelectedChoices();
26954
+ var _this$rendererRef8;
26955
+ (_this$rendererRef8 = this.rendererRef) == null || _this$rendererRef8.showRationalesForCurrentlySelectedChoices();
26977
26956
  };
26978
26957
  }
26979
26958
  componentDidMount() {
@@ -26982,6 +26961,14 @@ class Group extends React.Component {
26982
26961
  // the group with the correct number.
26983
26962
  this.forceUpdate();
26984
26963
  }
26964
+ getUserInput() {
26965
+ var _this$rendererRef9;
26966
+ return (_this$rendererRef9 = this.rendererRef) == null ? void 0 : _this$rendererRef9.getUserInput();
26967
+ }
26968
+ simpleValidate() {
26969
+ var _this$rendererRef10;
26970
+ return (_this$rendererRef10 = this.rendererRef) == null ? void 0 : _this$rendererRef10.score();
26971
+ }
26985
26972
  render() {
26986
26973
  var _this$rendererRef11;
26987
26974
  const apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
@@ -27249,15 +27236,18 @@ class ImageWidget extends React.Component {
27249
27236
  this.change = (...args) => {
27250
27237
  return change.apply(this, args);
27251
27238
  };
27252
- this.getUserInput = () => {
27253
- return null;
27254
- };
27255
- this.simpleValidate = rubric => {
27256
- return ImageWidget.validate(this.getUserInput(), rubric);
27257
- };
27258
27239
  this.focus = () => {};
27259
27240
  }
27260
- static validate(userInput, rubric) {
27241
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27242
+ static validate() {
27243
+ return noopValidator();
27244
+ }
27245
+ getUserInput() {
27246
+ return null;
27247
+ }
27248
+
27249
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27250
+ simpleValidate() {
27261
27251
  return noopValidator();
27262
27252
  }
27263
27253
  // no-op
@@ -27567,18 +27557,9 @@ class Interaction extends React.Component {
27567
27557
  this.change = (...args) => {
27568
27558
  return change.apply(this, args);
27569
27559
  };
27570
- this.getUserInput = () => {
27571
- // TODO(eater): Perhaps we want to be able to record the state of the
27572
- // user's interaction. Unfortunately sending all the props will
27573
- // probably make the attempt payload too large. So for now, don't send
27574
- // anything.
27575
- return {};
27576
- };
27577
- this.simpleValidate = rubric => {
27578
- return Interaction.validate(this.getUserInput(), rubric);
27579
- };
27580
27560
  }
27581
- static validate(state, rubric) {
27561
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27562
+ static validate() {
27582
27563
  return noopValidator();
27583
27564
  }
27584
27565
  UNSAFE_componentWillReceiveProps(nextProps) {
@@ -27599,6 +27580,18 @@ class Interaction extends React.Component {
27599
27580
  });
27600
27581
  }
27601
27582
  }
27583
+ getUserInput() {
27584
+ // TODO(eater): Perhaps we want to be able to record the state of the
27585
+ // user's interaction. Unfortunately sending all the props will
27586
+ // probably make the attempt payload too large. So for now, don't send
27587
+ // anything.
27588
+ return {};
27589
+ }
27590
+
27591
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27592
+ simpleValidate() {
27593
+ return noopValidator();
27594
+ }
27602
27595
  render() {
27603
27596
  const range = this.props.graph.range;
27604
27597
  let labels = this.props.graph.labels;
@@ -29649,18 +29642,33 @@ function GraphLockedLabelsLayer(props) {
29649
29642
  lockedFigures
29650
29643
  } = props;
29651
29644
  return lockedFigures.map((figure, i) => {
29652
- var _flags$mafs;
29645
+ var _flags$mafs, _flags$mafs2, _flags$mafs3, _flags$mafs4, _flags$mafs5;
29653
29646
  if (figure.type === "label") {
29654
29647
  return /*#__PURE__*/React.createElement(LockedLabel, _extends({
29655
29648
  key: `label-${i}`
29656
29649
  }, figure));
29657
29650
  }
29658
- if (flags != null && (_flags$mafs = flags["mafs"]) != null && _flags$mafs["locked-point-labels"] && figure.type === "point") {
29651
+ if (
29652
+ // Point flag + point type
29653
+ flags != null && (_flags$mafs = flags["mafs"]) != null && _flags$mafs["locked-point-labels"] && figure.type === "point" ||
29654
+ // Line flag + line type
29655
+ flags != null && (_flags$mafs2 = flags["mafs"]) != null && _flags$mafs2["locked-line-labels"] && figure.type === "line" ||
29656
+ // Vector flag + vector type
29657
+ flags != null && (_flags$mafs3 = flags["mafs"]) != null && _flags$mafs3["locked-vector-labels"] && figure.type === "vector" ||
29658
+ // Ellipse flag + ellipse type
29659
+ flags != null && (_flags$mafs4 = flags["mafs"]) != null && _flags$mafs4["locked-ellipse-labels"] && figure.type === "ellipse" ||
29660
+ // Function flag + function type
29661
+ flags != null && (_flags$mafs5 = flags["mafs"]) != null && _flags$mafs5["locked-function-labels"] && figure.type === "function") {
29662
+ var _figure$labels, _figure$points$0$labe, _figure$points$1$labe;
29659
29663
  return /*#__PURE__*/React.createElement(React.Fragment, {
29660
- key: `point-${i}`
29661
- }, figure.labels.map((label, j) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29662
- key: `locked-figure-${i}-label-${j}`
29663
- }, label))));
29664
+ key: i
29665
+ }, (_figure$labels = figure.labels) == null ? void 0 : _figure$labels.map((label, j) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29666
+ key: `${i}-label-${j}`
29667
+ }, label))), figure.type === "line" && /*#__PURE__*/React.createElement(React.Fragment, null, (_figure$points$0$labe = figure.points[0].labels) == null ? void 0 : _figure$points$0$labe.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29668
+ key: `locked-figure-${i}-point-1-label-${k}`
29669
+ }, label))), (_figure$points$1$labe = figure.points[1].labels) == null ? void 0 : _figure$points$1$labe.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29670
+ key: `locked-figure-${i}-point-2-label-${k}`
29671
+ }, label)))));
29664
29672
  }
29665
29673
  return null;
29666
29674
  });
@@ -30000,7 +30008,9 @@ const GraphLockedLayer = props => {
30000
30008
 
30001
30009
  const actions = {
30002
30010
  global: {
30003
- deleteIntent
30011
+ deleteIntent,
30012
+ changeInteractionMode,
30013
+ changeKeyboardInvitationVisibility
30004
30014
  },
30005
30015
  angle: {
30006
30016
  movePoint
@@ -30092,6 +30102,20 @@ function clickPoint(index) {
30092
30102
  index
30093
30103
  };
30094
30104
  }
30105
+ const CHANGE_INTERACTION_MODE = "point-change-interaction-mode";
30106
+ function changeInteractionMode(mode) {
30107
+ return {
30108
+ type: CHANGE_INTERACTION_MODE,
30109
+ mode
30110
+ };
30111
+ }
30112
+ const CHANGE_KEYBOARD_INVITATION_VISIBILITY = "change-keyboard-interaction-invitation-visibility";
30113
+ function changeKeyboardInvitationVisibility(shouldShow) {
30114
+ return {
30115
+ type: CHANGE_KEYBOARD_INVITATION_VISIBILITY,
30116
+ shouldShow
30117
+ };
30118
+ }
30095
30119
  const MOVE_ALL = "move-all";
30096
30120
  function moveAll(delta) {
30097
30121
  return {
@@ -31740,7 +31764,7 @@ function Protractor() {
31740
31764
  const [[xMin, xMax], [yMin, yMax]] = range;
31741
31765
  // Position the protractor in the center of the graph (horizontally), and
31742
31766
  // 95% of the way to the bottom of the graph (vertically).
31743
- const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.05)];
31767
+ const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.25)];
31744
31768
  const [center, setCenter] = useState(initialCenter);
31745
31769
  const [rotationHandleOffset, setRotationHandleOffset] = useState(centerToRotationHandle);
31746
31770
  const draggableRef = useRef(null);
@@ -31902,6 +31926,9 @@ const MafsGraph = props => {
31902
31926
  x: viewboxX,
31903
31927
  y: viewboxY
31904
31928
  };
31929
+ const {
31930
+ strings
31931
+ } = usePerseusI18n();
31905
31932
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
31906
31933
  value: {
31907
31934
  range: state.range,
@@ -31930,16 +31957,18 @@ const MafsGraph = props => {
31930
31957
  height
31931
31958
  },
31932
31959
  onKeyUp: event => {
31933
- if (event.key === "Backspace") {
31934
- var _graphRef$current;
31935
- dispatch(actions.global.deleteIntent());
31936
- (_graphRef$current = graphRef.current) == null || _graphRef$current.focus();
31937
- }
31960
+ handleKeyboardEvent(event, state, dispatch);
31938
31961
  },
31939
31962
  "aria-label": fullGraphAriaLabel,
31940
31963
  "aria-describedby": fullGraphAriaDescription ? descriptionId : undefined,
31941
31964
  ref: graphRef,
31942
- tabIndex: 0
31965
+ tabIndex: 0,
31966
+ onFocus: event => {
31967
+ handleFocusEvent(event, state, dispatch);
31968
+ },
31969
+ onBlur: event => {
31970
+ handleBlurEvent(event, state, dispatch);
31971
+ }
31943
31972
  }, fullGraphAriaDescription && /*#__PURE__*/React.createElement(View, {
31944
31973
  id: descriptionId,
31945
31974
  tabIndex: -1,
@@ -32001,35 +32030,75 @@ const MafsGraph = props => {
32001
32030
  }, /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), renderGraph({
32002
32031
  state,
32003
32032
  dispatch
32004
- })))))), renderGraphControls({
32033
+ }))))), state.type === "point" && state.showKeyboardInteractionInvitation && /*#__PURE__*/React.createElement(View, {
32034
+ style: {
32035
+ textAlign: "center",
32036
+ backgroundColor: "white",
32037
+ border: "1px solid #21242C52",
32038
+ padding: "16px 0",
32039
+ boxShadow: "0px 8px 8px 0px #21242C14",
32040
+ // This translates the box to the center of the
32041
+ // graph Then backs it off by half of its
32042
+ // overall height so it's perfectly centered
32043
+ top: "50%",
32044
+ transform: "translateY(-50%)"
32045
+ }
32046
+ }, /*#__PURE__*/React.createElement(LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
32005
32047
  state,
32006
32048
  dispatch,
32007
- width
32049
+ width,
32050
+ strings
32008
32051
  })));
32009
32052
  };
32010
- const renderPointGraphControls = props => /*#__PURE__*/React.createElement(View, {
32011
- style: {
32012
- flexDirection: "row",
32013
- width: props.width
32014
- }
32015
- }, props.state.showRemovePointButton && props.state.focusedPointIndex !== null && /*#__PURE__*/React.createElement(Button, {
32016
- id: REMOVE_BUTTON_ID,
32017
- kind: "secondary",
32018
- color: "destructive",
32019
- tabIndex: -1,
32020
- style: {
32021
- width: "100%",
32022
- marginLeft: "20px"
32023
- },
32024
- onClick: event => {
32025
- props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32026
- }
32027
- }, "Remove Point"));
32053
+ const renderPointGraphControls = props => {
32054
+ const {
32055
+ interactionMode,
32056
+ showRemovePointButton,
32057
+ focusedPointIndex
32058
+ } = props.state;
32059
+ const {
32060
+ strings
32061
+ } = props;
32062
+ const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
32063
+ return /*#__PURE__*/React.createElement(View, {
32064
+ style: {
32065
+ flexDirection: "row",
32066
+ width: props.width
32067
+ }
32068
+ }, interactionMode === "keyboard" && /*#__PURE__*/React.createElement(Button, {
32069
+ kind: "secondary",
32070
+ style: {
32071
+ width: "100%",
32072
+ marginLeft: "20px"
32073
+ },
32074
+ tabIndex: 0,
32075
+ onClick: () => {
32076
+ props.dispatch(actions.pointGraph.addPoint([0, 0]));
32077
+ }
32078
+ }, strings.addPoint), interactionMode === "mouse" && /*#__PURE__*/React.createElement(Button, {
32079
+ id: REMOVE_BUTTON_ID,
32080
+ kind: "secondary",
32081
+ color: "destructive"
32082
+ // This button is meant to be interacted with by the mouse only
32083
+ // Never allow learners to tab to this button
32084
+ ,
32085
+ tabIndex: -1,
32086
+ style: {
32087
+ width: "100%",
32088
+ marginLeft: "20px",
32089
+ visibility: shouldShowRemoveButton ? "visible" : "hidden"
32090
+ },
32091
+ onClick: event => {
32092
+ props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32093
+ }
32094
+ }, strings.removePoint));
32095
+ };
32028
32096
  const renderGraphControls = props => {
32029
32097
  const {
32030
32098
  state,
32031
32099
  dispatch,
32032
- width
32100
+ width,
32101
+ strings
32033
32102
  } = props;
32034
32103
  const {
32035
32104
  type
@@ -32040,7 +32109,8 @@ const renderGraphControls = props => {
32040
32109
  return renderPointGraphControls({
32041
32110
  state,
32042
32111
  dispatch,
32043
- width
32112
+ width,
32113
+ strings
32044
32114
  });
32045
32115
  }
32046
32116
  return null;
@@ -32048,6 +32118,34 @@ const renderGraphControls = props => {
32048
32118
  return null;
32049
32119
  }
32050
32120
  };
32121
+ function handleFocusEvent(event, state, dispatch) {
32122
+ if (state.type === "point" && state.numPoints === "unlimited") {
32123
+ if (event.target.classList.contains("mafs-graph") && state.interactionMode === "mouse") {
32124
+ dispatch(actions.global.changeKeyboardInvitationVisibility(true));
32125
+ }
32126
+ }
32127
+ }
32128
+ function handleBlurEvent(event, state, dispatch) {
32129
+ if (state.type === "point" && state.numPoints === "unlimited") {
32130
+ dispatch(actions.global.changeKeyboardInvitationVisibility(false));
32131
+ }
32132
+ }
32133
+ function handleKeyboardEvent(event, state, dispatch) {
32134
+ if (state.type === "point" && state.numPoints === "unlimited") {
32135
+ if (event.key === "Backspace") {
32136
+ dispatch(actions.global.deleteIntent());
32137
+
32138
+ // After removing a point blur
32139
+ // It would be nice if this could focus on the graph but doing so
32140
+ // would trigger the message to prompt a learner to enter keyboard mode
32141
+ document.activeElement.blur();
32142
+ } else if (event.shiftKey && event.key === "Enter") {
32143
+ dispatch(actions.global.changeInteractionMode("keyboard"));
32144
+ } else if (state.interactionMode === "keyboard" && event.key === "a") {
32145
+ dispatch(actions.pointGraph.addPoint([0, 0]));
32146
+ }
32147
+ }
32148
+ }
32051
32149
 
32052
32150
  // Calculate the difference between the min and max values of a range
32053
32151
  const getRangeDiff = range => {
@@ -32212,7 +32310,9 @@ function initializeGraphState(params) {
32212
32310
  coords: getPointCoords(graph, range, step),
32213
32311
  numPoints: graph.numPoints || 0,
32214
32312
  focusedPointIndex: null,
32215
- showRemovePointButton: false
32313
+ showRemovePointButton: false,
32314
+ interactionMode: "mouse",
32315
+ showKeyboardInteractionInvitation: false
32216
32316
  });
32217
32317
  case "circle":
32218
32318
  return _extends({}, shared, {
@@ -32482,6 +32582,10 @@ function interactiveGraphReducer(state, action) {
32482
32582
  return doDeleteIntent(state);
32483
32583
  case CLICK_POINT:
32484
32584
  return doClickPoint(state, action);
32585
+ case CHANGE_INTERACTION_MODE:
32586
+ return doChangeInteractionMode(state, action);
32587
+ case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
32588
+ return doChangeKeyboardInvitationVisibility(state, action);
32485
32589
  default:
32486
32590
  throw new UnreachableCaseError(action);
32487
32591
  }
@@ -32530,6 +32634,31 @@ function doClickPoint(state, action) {
32530
32634
  }
32531
32635
  return state;
32532
32636
  }
32637
+ function doChangeInteractionMode(state, action) {
32638
+ if (state.type !== "point") {
32639
+ return state;
32640
+ }
32641
+ if (state.numPoints === "unlimited") {
32642
+ const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
32643
+ return _extends({}, state, {
32644
+ interactionMode: action.mode,
32645
+ showKeyboardInteractionInvitation: nextKeyboardInvitation
32646
+ });
32647
+ }
32648
+ return state;
32649
+ }
32650
+ function doChangeKeyboardInvitationVisibility(state, action) {
32651
+ if (state.type !== "point") {
32652
+ return state;
32653
+ }
32654
+ if (state.numPoints === "unlimited") {
32655
+ return _extends({}, state, {
32656
+ showKeyboardInteractionInvitation: action.shouldShow,
32657
+ hasBeenInteractedWith: true
32658
+ });
32659
+ }
32660
+ return state;
32661
+ }
32533
32662
  function doMovePointInFigure(state, action) {
32534
32663
  switch (state.type) {
32535
32664
  case "segment":
@@ -33266,7 +33395,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33266
33395
  const snapTo = graph.type === "polygon" ? graph.snapTo : null;
33267
33396
  const showAngles = graph.type === "polygon" ? graph.showAngles : null;
33268
33397
  const showSides = graph.type === "polygon" ? graph.showSides : null;
33269
- const startCoords = "startCoords" in graph ? graph.startCoords : null;
33398
+ const startCoords = "startCoords" in graph ? graph.startCoords : undefined;
33270
33399
  const originalPropsRef = useRef(props);
33271
33400
  const latestPropsRef = useLatestRef(props);
33272
33401
  useEffect(() => {
@@ -34565,20 +34694,19 @@ class InteractiveGraph extends React.Component {
34565
34694
  super(...args);
34566
34695
  this.legacyGraphRef = /*#__PURE__*/React.createRef();
34567
34696
  this.mafsRef = /*#__PURE__*/React.createRef();
34568
- this.getUserInput = () => {
34569
- if (this.mafsRef.current) {
34570
- var _this$mafsRef$current, _this$mafsRef$current2;
34571
- return (_this$mafsRef$current = (_this$mafsRef$current2 = this.mafsRef.current).getUserInput) == null ? void 0 : _this$mafsRef$current.call(_this$mafsRef$current2);
34572
- }
34573
- if (this.legacyGraphRef.current) {
34574
- return this.legacyGraphRef.current.getUserInput();
34575
- }
34576
- throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
34577
- };
34697
+ }
34698
+ getUserInput() {
34699
+ var _this$mafsRef$current, _this$legacyGraphRef$;
34700
+ if ((_this$mafsRef$current = this.mafsRef.current) != null && _this$mafsRef$current.getUserInput) {
34701
+ return this.mafsRef.current.getUserInput();
34702
+ }
34703
+ if ((_this$legacyGraphRef$ = this.legacyGraphRef.current) != null && _this$legacyGraphRef$.getUserInput) {
34704
+ return this.legacyGraphRef.current.getUserInput();
34705
+ }
34706
+ throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
34578
34707
  }
34579
34708
  simpleValidate(rubric) {
34580
- var _this$getUserInput;
34581
- return InteractiveGraph.validate((_this$getUserInput = this.getUserInput) == null ? void 0 : _this$getUserInput.call(this), rubric, this);
34709
+ return InteractiveGraph.validate(this.getUserInput(), rubric, this);
34582
34710
  }
34583
34711
  render() {
34584
34712
  var _this$props$apiOption;
@@ -36256,6 +36384,57 @@ const styles$h = StyleSheet.create({
36256
36384
  }
36257
36385
  });
36258
36386
 
36387
+ // Question state for marker as result of user selected answers.
36388
+
36389
+ function scoreMarker(marker) {
36390
+ const score = {
36391
+ hasAnswers: false,
36392
+ isCorrect: false
36393
+ };
36394
+ if (marker.selected && marker.selected.length > 0) {
36395
+ score.hasAnswers = true;
36396
+ }
36397
+ if (marker.answers.length > 0) {
36398
+ if (marker.selected && marker.selected.length === marker.answers.length) {
36399
+ // All correct answers are selected by the user.
36400
+ score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36401
+ }
36402
+ } else if (!marker.selected || marker.selected.length === 0) {
36403
+ // Correct as no answers should be selected by the user.
36404
+ score.isCorrect = true;
36405
+ }
36406
+ return score;
36407
+ }
36408
+ function labelImageValidator(state, rubric) {
36409
+ let numAnswered = 0;
36410
+ let numCorrect = 0;
36411
+ for (const marker of state.markers) {
36412
+ const score = scoreMarker(marker);
36413
+ if (score.hasAnswers) {
36414
+ numAnswered++;
36415
+ }
36416
+ if (score.isCorrect) {
36417
+ numCorrect++;
36418
+ }
36419
+ }
36420
+
36421
+ // We expect all question markers to be answered before grading.
36422
+ if (numAnswered !== state.markers.length) {
36423
+ return {
36424
+ type: "invalid",
36425
+ message: null
36426
+ };
36427
+ }
36428
+ return {
36429
+ type: "points",
36430
+ // Markers with no expected answers are graded as correct if user
36431
+ // makes no answer selection.
36432
+ earned: numCorrect === state.markers.length ? 1 : 0,
36433
+ total: 1,
36434
+ message: null
36435
+ };
36436
+ }
36437
+
36259
36438
  const BringToFront = {
36260
36439
  boxShadow: `0 8px 8px ${color.offBlack64}`,
36261
36440
  zIndex: 1000
@@ -36514,57 +36693,11 @@ const styles$f = StyleSheet.create({
36514
36693
  * No diagonals allowed.
36515
36694
  */
36516
36695
 
36696
+ // TODO: should this be using WidgetProps / PerseusLabelImageWidgetOptions?
36697
+
36517
36698
  class LabelImage extends React.Component {
36518
- static gradeMarker(marker) {
36519
- const score = {
36520
- hasAnswers: false,
36521
- isCorrect: false
36522
- };
36523
- if (marker.selected && marker.selected.length > 0) {
36524
- // @ts-expect-error - TS2540 - Cannot assign to 'hasAnswers' because it is a read-only property.
36525
- score.hasAnswers = true;
36526
- }
36527
- if (marker.answers.length > 0) {
36528
- if (marker.selected && marker.selected.length === marker.answers.length) {
36529
- // All correct answers are selected by the user.
36530
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
36531
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36532
- }
36533
- } else if (!marker.selected || marker.selected.length === 0) {
36534
- // Correct as no answers should be selected by the user.
36535
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
36536
- score.isCorrect = true;
36537
- }
36538
- return score;
36539
- }
36540
36699
  static validate(state, rubric) {
36541
- let numAnswered = 0;
36542
- let numCorrect = 0;
36543
- for (const marker of state.markers) {
36544
- const score = LabelImage.gradeMarker(marker);
36545
- if (score.hasAnswers) {
36546
- numAnswered++;
36547
- }
36548
- if (score.isCorrect) {
36549
- numCorrect++;
36550
- }
36551
- }
36552
-
36553
- // We expect all question markers to be answered before grading.
36554
- if (numAnswered !== state.markers.length) {
36555
- return {
36556
- type: "invalid",
36557
- message: null
36558
- };
36559
- }
36560
- return {
36561
- type: "points",
36562
- // Markers with no expected answers are graded as correct if user
36563
- // makes no answer selection.
36564
- earned: numCorrect === state.markers.length ? 1 : 0,
36565
- total: 1,
36566
- message: null
36567
- };
36700
+ return labelImageValidator(state);
36568
36701
  }
36569
36702
 
36570
36703
  /**
@@ -36762,7 +36895,7 @@ class LabelImage extends React.Component {
36762
36895
  this._mounted = false;
36763
36896
  }
36764
36897
  simpleValidate(rubric) {
36765
- return LabelImage.validate(this.getUserInput(), rubric);
36898
+ return labelImageValidator(this.getUserInput());
36766
36899
  }
36767
36900
  getUserInput() {
36768
36901
  const {
@@ -36780,7 +36913,7 @@ class LabelImage extends React.Component {
36780
36913
  onChange
36781
36914
  } = this.props;
36782
36915
  const updatedMarkers = markers.map(marker => {
36783
- const score = LabelImage.gradeMarker(marker);
36916
+ const score = scoreMarker(marker);
36784
36917
  return _extends({}, marker, {
36785
36918
  // Reveal correctness state for markers with answers.
36786
36919
  showCorrectness: score.hasAnswers ? score.isCorrect ? "correct" : "incorrect" : undefined
@@ -36927,7 +37060,7 @@ class LabelImage extends React.Component {
36927
37060
  bottom: "top"
36928
37061
  }[markerPosition];
36929
37062
  }
36930
- const score = LabelImage.gradeMarker(marker);
37063
+ const score = scoreMarker(marker);
36931
37064
  // Once the question is answered, show markers
36932
37065
  // with correct answers, otherwise passthrough
36933
37066
  // the correctness state.
@@ -37126,9 +37259,7 @@ const LabelImageWithDependencies = /*#__PURE__*/React.forwardRef((props, ref) =>
37126
37259
  // methods and instead adjust Peresus to provide these facilities through
37127
37260
  // instance methods on our Renderers.
37128
37261
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
37129
- LabelImageWithDependencies.validate = LabelImage.validate;
37130
- // @ts-expect-error - TS2339 - Property 'gradeMarker' does not exist on type
37131
- LabelImageWithDependencies.gradeMarker = LabelImage.gradeMarker;
37262
+ LabelImageWithDependencies.validate = labelImageValidator;
37132
37263
  var LabelImage$1 = {
37133
37264
  name: "label-image",
37134
37265
  displayName: "Label Image",
@@ -37908,10 +38039,10 @@ class Matcher extends React.Component {
37908
38039
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
37909
38040
  this.refs.right.moveOptionToIndex(option, index);
37910
38041
  };
37911
- this.simpleValidate = rubric => {
37912
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matcher'.
37913
- return Matcher.validate(this.getUserInput(), rubric);
37914
- };
38042
+ }
38043
+ simpleValidate(rubric) {
38044
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matcher'.
38045
+ return Matcher.validate(this.getUserInput(), rubric);
37915
38046
  }
37916
38047
  render() {
37917
38048
  // To minimize layout shift, we display a spinner until our math
@@ -38275,14 +38406,6 @@ class Matrix extends React.Component {
38275
38406
  }, cb);
38276
38407
  this.props.trackInteraction();
38277
38408
  };
38278
- this.getUserInput = () => {
38279
- return {
38280
- answers: this.props.answers
38281
- };
38282
- };
38283
- this.simpleValidate = rubric => {
38284
- return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
38285
- };
38286
38409
  }
38287
38410
  static validate(state, rubric, strings) {
38288
38411
  const solution = rubric.answers;
@@ -38300,7 +38423,9 @@ class Matrix extends React.Component {
38300
38423
  hasEmptyCell = true;
38301
38424
  }
38302
38425
  if (!incorrectSize) {
38303
- const validator = createValidator(solution[row][col], {
38426
+ const validator = createValidator(
38427
+ // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
38428
+ solution[row][col], {
38304
38429
  simplify: true
38305
38430
  }, strings);
38306
38431
  const result = validator(supplied[row][col]);
@@ -38339,6 +38464,14 @@ class Matrix extends React.Component {
38339
38464
  // Used in the `onBlur` and `onFocus` handlers
38340
38465
  this.cursorPosition = [0, 0];
38341
38466
  }
38467
+ getUserInput() {
38468
+ return {
38469
+ answers: this.props.answers
38470
+ };
38471
+ }
38472
+ simpleValidate(rubric) {
38473
+ return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
38474
+ }
38342
38475
  render() {
38343
38476
  // Set the input sizes through JS so we can control the size of the
38344
38477
  // brackets. (If we set them in CSS we won't know values until the
@@ -38629,9 +38762,8 @@ const Measurer = createReactClass({
38629
38762
  getUserInput: function () {
38630
38763
  return {};
38631
38764
  },
38632
- simpleValidate: function (rubric) {
38633
- // TODO(joel) - I don't understand how this is useful!
38634
- return Measurer.validate(this.getUserInput(), rubric);
38765
+ simpleValidate: function () {
38766
+ return noopValidator(1);
38635
38767
  },
38636
38768
  focus: $.noop,
38637
38769
  render: function () {
@@ -38660,14 +38792,7 @@ const Measurer = createReactClass({
38660
38792
  }
38661
38793
  });
38662
38794
  _.extend(Measurer, {
38663
- validate: function (state, rubric) {
38664
- return {
38665
- type: "points",
38666
- earned: 1,
38667
- total: 1,
38668
- message: null
38669
- };
38670
- }
38795
+ validate: noopValidator(1)
38671
38796
  });
38672
38797
  const propUpgrades = {
38673
38798
  "1": v0props => {
@@ -39557,25 +39682,17 @@ class Molecule extends React.Component {
39557
39682
  }
39558
39683
  Molecule.contextType = PerseusI18nContext;
39559
39684
  class MoleculeWidget extends React.Component {
39560
- constructor(...args) {
39561
- super(...args);
39562
- this.simpleValidate = () => {
39563
- return {
39564
- type: "points",
39565
- earned: 0,
39566
- total: 0,
39567
- message: null
39568
- };
39569
- };
39570
- this.getUserInput = () => {
39571
- return [];
39572
- };
39573
- this.validate = (state, rubric) => {
39574
- // TODO(colin): this is here as part of the interface for a component.
39575
- // Figure out if there is something more appropriate that this should
39576
- // return.
39577
- return noopValidator();
39578
- };
39685
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
39686
+ simpleValidate() {
39687
+ return noopValidator();
39688
+ }
39689
+ getUserInput() {
39690
+ return [];
39691
+ }
39692
+
39693
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
39694
+ validate() {
39695
+ return noopValidator();
39579
39696
  }
39580
39697
  render() {
39581
39698
  return /*#__PURE__*/React.createElement(Molecule, {
@@ -39720,6 +39837,9 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
39720
39837
  });
39721
39838
  return results;
39722
39839
  });
39840
+
39841
+ // TODO: most widgets use some like Widget<Something, PerseusNumberLineWidgetOptions>
39842
+ // should this one?
39723
39843
  class NumberLine extends React.Component {
39724
39844
  constructor(..._args) {
39725
39845
  super(..._args);
@@ -39999,19 +40119,19 @@ class NumberLine extends React.Component {
39999
40119
  arrows: "->"
40000
40120
  });
40001
40121
  };
40002
- this.getUserInput = () => {
40003
- return {
40004
- numLinePosition: this.props.numLinePosition,
40005
- rel: this.props.isInequality ? this.props.rel : "eq",
40006
- numDivisions: this.props.numDivisions,
40007
- divisionRange: this.props.divisionRange
40008
- };
40009
- };
40010
- this.simpleValidate = rubric => {
40011
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof NumberLine'.
40012
- return NumberLine.validate(this.getUserInput(), rubric);
40122
+ }
40123
+ getUserInput() {
40124
+ return {
40125
+ numLinePosition: this.props.numLinePosition,
40126
+ rel: this.props.isInequality ? this.props.rel : "eq",
40127
+ numDivisions: this.props.numDivisions,
40128
+ divisionRange: this.props.divisionRange
40013
40129
  };
40014
40130
  }
40131
+ simpleValidate(rubric) {
40132
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof NumberLine'.
40133
+ return NumberLine.validate(this.getUserInput(), rubric);
40134
+ }
40015
40135
  render() {
40016
40136
  const {
40017
40137
  strings
@@ -40086,6 +40206,8 @@ _.extend(NumberLine, {
40086
40206
  const correctRel = rubric.correctRel || "eq";
40087
40207
  const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
40088
40208
  const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
40209
+
40210
+ // TODO: I don't think isTickCrtl is a thing anymore
40089
40211
  if (state.isTickCrtl && outsideAllowedRange) {
40090
40212
  return {
40091
40213
  type: "invalid",
@@ -40342,8 +40464,8 @@ Card.defaultProps = {
40342
40464
  animating: false,
40343
40465
  linterContext: linterContextDefault
40344
40466
  };
40345
- const NORMAL = "normal",
40346
- HORIZONTAL = "horizontal";
40467
+ const NORMAL = "normal";
40468
+ const HORIZONTAL = "horizontal";
40347
40469
  class Orderer extends React.Component {
40348
40470
  constructor(...args) {
40349
40471
  super(...args);
@@ -40584,17 +40706,6 @@ class Orderer extends React.Component {
40584
40706
  current: list
40585
40707
  });
40586
40708
  };
40587
- this.getUserInput = () => {
40588
- return {
40589
- current: _.map(this.props.current, function (v) {
40590
- return v.content;
40591
- })
40592
- };
40593
- };
40594
- this.simpleValidate = rubric => {
40595
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Orderer'.
40596
- return Orderer.validate(this.getUserInput(), rubric);
40597
- };
40598
40709
  }
40599
40710
  UNSAFE_componentWillReceiveProps(nextProps) {
40600
40711
  if (!_.isEqual(this.props.current, nextProps.current)) {
@@ -40603,6 +40714,17 @@ class Orderer extends React.Component {
40603
40714
  });
40604
40715
  }
40605
40716
  }
40717
+ getUserInput() {
40718
+ return {
40719
+ current: _.map(this.props.current, function (v) {
40720
+ return v.content;
40721
+ })
40722
+ };
40723
+ }
40724
+ simpleValidate(rubric) {
40725
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Orderer'.
40726
+ return Orderer.validate(this.getUserInput(), rubric);
40727
+ }
40606
40728
  render() {
40607
40729
  // This is the card we are currently dragging
40608
40730
  const dragging = this.state.dragging && /*#__PURE__*/React.createElement(Card
@@ -40716,14 +40838,14 @@ Orderer.defaultProps = {
40716
40838
  linterContext: linterContextDefault
40717
40839
  };
40718
40840
  _.extend(Orderer, {
40719
- validate: function (state, rubric) {
40720
- if (state.current.length === 0) {
40841
+ validate: function (userInput, rubric) {
40842
+ if (userInput.current.length === 0) {
40721
40843
  return {
40722
40844
  type: "invalid",
40723
40845
  message: null
40724
40846
  };
40725
40847
  }
40726
- const correct = _.isEqual(state.current, _.pluck(rubric.correctOptions, "content"));
40848
+ const correct = _.isEqual(userInput.current, _.pluck(rubric.correctOptions, "content"));
40727
40849
  return {
40728
40850
  type: "points",
40729
40851
  earned: correct ? 1 : 0,
@@ -42539,7 +42661,8 @@ class Passage extends React.Component {
42539
42661
  });
42540
42662
  };
42541
42663
  }
42542
- static validate(state, rubric) {
42664
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42665
+ static validate() {
42543
42666
  return noopValidator();
42544
42667
  }
42545
42668
  componentDidMount() {
@@ -42755,8 +42878,10 @@ class Passage extends React.Component {
42755
42878
  getUserInput() {
42756
42879
  return null;
42757
42880
  }
42758
- simpleValidate(rubric) {
42759
- return Passage.validate(this.getUserInput(), rubric);
42881
+
42882
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42883
+ simpleValidate() {
42884
+ return noopValidator();
42760
42885
  }
42761
42886
 
42762
42887
  /**
@@ -42919,19 +43044,21 @@ class PassageRefTarget extends React.Component {
42919
43044
  constructor(..._args) {
42920
43045
  super(..._args);
42921
43046
  // TODO passage-ref-target isn't interactive; remove
42922
- this.getUserInput = () => {
42923
- return;
42924
- };
42925
- // TODO passage-ref-target isn't interactive; remove
42926
43047
  this.change = (...args) => {
42927
43048
  return change.apply(this, args);
42928
43049
  };
42929
- // TODO passage-ref-target isn't interactive; remove
42930
- this.simpleValidate = rubric => {
42931
- return PassageRefTarget.validate(this.getUserInput(), rubric);
42932
- };
42933
43050
  }
42934
- static validate(state, rubric) {
43051
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43052
+ static validate() {
43053
+ return noopValidator();
43054
+ }
43055
+
43056
+ // TODO passage-ref-target isn't interactive; remove
43057
+ getUserInput() {
43058
+ return null;
43059
+ }
43060
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43061
+ simpleValidate() {
42935
43062
  return noopValidator();
42936
43063
  }
42937
43064
  render() {
@@ -42974,9 +43101,6 @@ var PassageRefTarget$1 = {
42974
43101
  * This is a PhET simulation widget. It is used for rendering simulations
42975
43102
  * from https://phet.colorado.edu/.
42976
43103
  */
42977
-
42978
- // For returning user input, but currently the PhET widget
42979
- // does not support accessing user input
42980
43104
  // This renders the PhET sim
42981
43105
  class PhetSimulation extends React.Component {
42982
43106
  constructor(props) {
@@ -43025,6 +43149,8 @@ class PhetSimulation extends React.Component {
43025
43149
  await this.updateSimState(this.props.url);
43026
43150
  }
43027
43151
  }
43152
+
43153
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43028
43154
  getUserInput() {
43029
43155
  return null;
43030
43156
  }
@@ -43910,13 +44036,6 @@ class Plotter extends React.Component {
43910
44036
  });
43911
44037
  });
43912
44038
  };
43913
- this.getUserInput = () => {
43914
- return this.state.values;
43915
- };
43916
- this.simpleValidate = rubric => {
43917
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Plotter'.
43918
- return Plotter.validate(this.getUserInput(), rubric);
43919
- };
43920
44039
  }
43921
44040
  componentDidMount() {
43922
44041
  this._isMounted = true;
@@ -43941,6 +44060,13 @@ class Plotter extends React.Component {
43941
44060
  componentWillUnmount() {
43942
44061
  this._isMounted = false;
43943
44062
  }
44063
+ getUserInput() {
44064
+ return this.state.values;
44065
+ }
44066
+ simpleValidate(rubric) {
44067
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Plotter'.
44068
+ return Plotter.validate(this.getUserInput(), rubric);
44069
+ }
43944
44070
  render() {
43945
44071
  // TODO(kevinb) actually compute the size of the graphie correctly and
43946
44072
  // make it that size so we don't have to add extra padding. The value
@@ -43973,8 +44099,8 @@ Plotter.defaultProps = {
43973
44099
  labelInterval: 1
43974
44100
  };
43975
44101
  _.extend(Plotter, {
43976
- validate: function (guess, rubric) {
43977
- if (deepEq(guess, rubric.starting)) {
44102
+ validate: function (userInput, rubric) {
44103
+ if (deepEq(userInput, rubric.starting)) {
43978
44104
  return {
43979
44105
  type: "invalid",
43980
44106
  message: null
@@ -43982,7 +44108,7 @@ _.extend(Plotter, {
43982
44108
  }
43983
44109
  return {
43984
44110
  type: "points",
43985
- earned: deepEq(guess, rubric.correct) ? 1 : 0,
44111
+ earned: deepEq(userInput, rubric.correct) ? 1 : 0,
43986
44112
  total: 1,
43987
44113
  message: null
43988
44114
  };
@@ -44045,6 +44171,28 @@ var PythonProgram$1 = {
44045
44171
  widget: PythonProgram
44046
44172
  };
44047
44173
 
44174
+ function sorterValidator(userInput, rubric) {
44175
+ // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44176
+ // prevents the "Check" button from becoming active. We want the user
44177
+ // to make a change before trying to move forward. This makes an
44178
+ // assumption that the initial order isn't the correct order! However,
44179
+ // this should be rare if it happens, and interacting with the list
44180
+ // will enable the button, so they won't be locked out of progressing.
44181
+ if (!userInput.changed) {
44182
+ return {
44183
+ type: "invalid",
44184
+ message: null
44185
+ };
44186
+ }
44187
+ const correct = Util.deepEq(userInput.options, rubric.correct);
44188
+ return {
44189
+ type: "points",
44190
+ earned: correct ? 1 : 0,
44191
+ total: 1,
44192
+ message: null
44193
+ };
44194
+ }
44195
+
44048
44196
  const {
44049
44197
  shuffle
44050
44198
  } = Util;
@@ -44074,42 +44222,14 @@ class Sorter extends React.Component {
44074
44222
  }, 0);
44075
44223
  });
44076
44224
  };
44077
- this.getUserInput = () => {
44078
- // eslint-disable-next-line react/no-string-refs
44079
- // @ts-expect-error - TS2339 - Property 'getOptions' does not exist on type 'ReactInstance'.
44080
- return {
44081
- options: this.refs.sortable.getOptions()
44082
- };
44083
- };
44084
44225
  this.moveOptionToIndex = (option, index) => {
44085
44226
  // eslint-disable-next-line react/no-string-refs
44086
44227
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
44087
44228
  this.refs.sortable.moveOptionToIndex(option, index);
44088
44229
  };
44089
- this.simpleValidate = rubric => {
44090
- // If this widget hasn't been changed yet, we treat it as "empty" which
44091
- // prevents the "Check" button from becoming active. We want the user
44092
- // to make a change before trying to move forward. This makes an
44093
- // assumption that the initial order isn't the correct order! However,
44094
- // this should be rare if it happens, and interacting with the list
44095
- // will enable the button, so they won't be locked out of progressing.
44096
- if (!this.state.changed) {
44097
- return {
44098
- type: "invalid",
44099
- message: null
44100
- };
44101
- }
44102
- return Sorter.validate(this.getUserInput(), rubric);
44103
- };
44104
44230
  }
44105
- static validate(state, rubric) {
44106
- const correct = _.isEqual(state.options, rubric.correct);
44107
- return {
44108
- type: "points",
44109
- earned: correct ? 1 : 0,
44110
- total: 1,
44111
- message: null
44112
- };
44231
+ static validate(userInput, rubric) {
44232
+ return sorterValidator(userInput, rubric);
44113
44233
  }
44114
44234
  componentDidMount() {
44115
44235
  this._isMounted = true;
@@ -44117,6 +44237,18 @@ class Sorter extends React.Component {
44117
44237
  componentWillUnmount() {
44118
44238
  this._isMounted = false;
44119
44239
  }
44240
+ getUserInput() {
44241
+ // eslint-disable-next-line react/no-string-refs
44242
+ // @ts-expect-error - TS2339 - Property 'getOptions' does not exist on type 'ReactInstance'.
44243
+ const options = this.refs.sortable.getOptions();
44244
+ return {
44245
+ options,
44246
+ changed: this.state.changed
44247
+ };
44248
+ }
44249
+ simpleValidate(rubric) {
44250
+ return sorterValidator(this.getUserInput(), rubric);
44251
+ }
44120
44252
  render() {
44121
44253
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
44122
44254
  const {
@@ -44189,9 +44321,6 @@ class Table extends React.Component {
44189
44321
  this._getColumns = () => {
44190
44322
  return this.props.answers[0].length;
44191
44323
  };
44192
- this.getUserInput = () => {
44193
- return _.map(this.props.answers, _.clone);
44194
- };
44195
44324
  this.onValueChange = (row, column, eventOrValue) => {
44196
44325
  const answers = _.map(this.props.answers, _.clone);
44197
44326
 
@@ -44211,10 +44340,6 @@ class Table extends React.Component {
44211
44340
  headers: headers
44212
44341
  });
44213
44342
  };
44214
- this.simpleValidate = rubric => {
44215
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
44216
- return Table.validate(this.getUserInput(), rubric, this.context.strings);
44217
- };
44218
44343
  this._handleFocus = inputPath => {
44219
44344
  this.props.onFocus(inputPath);
44220
44345
  };
@@ -44281,6 +44406,13 @@ class Table extends React.Component {
44281
44406
  }, cb);
44282
44407
  };
44283
44408
  }
44409
+ getUserInput() {
44410
+ return _.map(this.props.answers, _.clone);
44411
+ }
44412
+ simpleValidate(rubric) {
44413
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
44414
+ return Table.validate(this.getUserInput(), rubric, this.context.strings);
44415
+ }
44284
44416
  render() {
44285
44417
  const rows = this._getRows();
44286
44418
  const columns = this._getColumns();
@@ -44529,9 +44661,6 @@ class Video extends React.Component {
44529
44661
  this.getUserInput = () => {
44530
44662
  return null;
44531
44663
  };
44532
- this.simpleValidate = rubric => {
44533
- return Video.validate(null, rubric);
44534
- };
44535
44664
  this.change = (...args) => {
44536
44665
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
44537
44666
  return change.apply(this, args);
@@ -44541,8 +44670,12 @@ class Video extends React.Component {
44541
44670
  * This is the widget's grading function.
44542
44671
  * Points for videos are tallied by the embedded video itself, in the case
44543
44672
  * of Khan Academy videos.
44544
- */
44545
- static validate(userInput, rubric) {
44673
+ */ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
44674
+ static validate() {
44675
+ return noopValidator();
44676
+ }
44677
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
44678
+ simpleValidate() {
44546
44679
  return noopValidator();
44547
44680
  }
44548
44681
  render() {
@@ -44648,7 +44781,7 @@ const init = function init(options) {
44648
44781
 
44649
44782
  // This file is processed by a Rollup plugin (replace) to inject the production
44650
44783
  const libName = "@khanacademy/perseus";
44651
- const libVersion = "34.0.0";
44784
+ const libVersion = "35.0.0";
44652
44785
  addLibraryVersionToPerseusDebug(libName, libVersion);
44653
44786
 
44654
44787
  const apiVersion = {
@@ -45199,9 +45332,6 @@ const defaultContext = {
45199
45332
  };
45200
45333
  const context = /*#__PURE__*/React.createContext(defaultContext);
45201
45334
 
45202
- const {
45203
- mapObject
45204
- } = objective_;
45205
45335
  class ServerItemRenderer extends React.Component {
45206
45336
  constructor(props) {
45207
45337
  super(props);
@@ -45421,20 +45551,6 @@ class ServerItemRenderer extends React.Component {
45421
45551
  return this.questionRenderer.getWidgetIds();
45422
45552
  }
45423
45553
 
45424
- /**
45425
- * Returns an object mapping from widget ID to KE-style score.
45426
- * The keys of this object are the values of the array returned
45427
- * from `getWidgetIds`.
45428
- */
45429
- scoreWidgets() {
45430
- const qScore = this.questionRenderer.scoreWidgets();
45431
- const qGuess = this.questionRenderer.getUserInputForWidgets();
45432
- const state = this.questionRenderer.getSerializedState();
45433
- return mapObject(qScore, (score, id) => {
45434
- return Util.keScoreFromPerseusScore(score, qGuess[id], state);
45435
- });
45436
- }
45437
-
45438
45554
  /**
45439
45555
  * Get a representation of the current state of the item.
45440
45556
  */