@khanacademy/perseus 52.0.1 → 53.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/components/number-input.d.ts +2 -0
  2. package/dist/components/range-input.d.ts +1 -0
  3. package/dist/components/svg-image.d.ts +2 -2
  4. package/dist/es/index.css +0 -30
  5. package/dist/es/index.js +893 -1042
  6. package/dist/es/index.js.map +1 -1
  7. package/dist/es/strings.js +42 -6
  8. package/dist/es/strings.js.map +1 -1
  9. package/dist/index.css +0 -30
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.js +913 -2263
  12. package/dist/index.js.map +1 -1
  13. package/dist/perseus-api.d.ts +0 -1
  14. package/dist/renderer.d.ts +1 -1
  15. package/dist/server-item-renderer.d.ts +0 -3
  16. package/dist/strings.d.ts +36 -0
  17. package/dist/strings.js +60 -6
  18. package/dist/strings.js.map +1 -1
  19. package/dist/styles/constants.d.ts +0 -2
  20. package/dist/types.d.ts +3 -32
  21. package/dist/util/math-utils.d.ts +1 -0
  22. package/dist/util/scoring.d.ts +0 -15
  23. package/dist/util/test-utils.d.ts +1 -1
  24. package/dist/widget-ai-utils/mock-widget/prompt-utils.d.ts +1 -1
  25. package/dist/widget-container.d.ts +1 -1
  26. package/dist/widgets/categorizer/categorizer.d.ts +0 -3
  27. package/dist/widgets/cs-program/cs-program.d.ts +2 -3
  28. package/dist/widgets/definition/definition.d.ts +0 -2
  29. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +0 -1
  30. package/dist/widgets/dropdown/dropdown.d.ts +2 -4
  31. package/dist/widgets/explanation/explanation.d.ts +0 -2
  32. package/dist/widgets/expression/expression.d.ts +1 -11
  33. package/dist/widgets/grapher/grapher.d.ts +2 -2
  34. package/dist/widgets/group/group.d.ts +0 -4
  35. package/dist/widgets/iframe/iframe.d.ts +2 -3
  36. package/dist/widgets/image/image.d.ts +0 -3
  37. package/dist/widgets/input-number/input-number.d.ts +0 -3
  38. package/dist/widgets/interaction/interaction.d.ts +0 -1
  39. package/dist/widgets/interactive-graph.d.ts +2 -3
  40. package/dist/widgets/interactive-graphs/backgrounds/axis-ticks.d.ts +1 -0
  41. package/dist/widgets/interactive-graphs/graphs/polygon.d.ts +3 -2
  42. package/dist/widgets/interactive-graphs/graphs/screenreader-text.d.ts +1 -1
  43. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +16 -1
  44. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +0 -2
  45. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +0 -2
  46. package/dist/widgets/label-image/label-image.d.ts +2 -2
  47. package/dist/widgets/matcher/matcher.d.ts +0 -2
  48. package/dist/widgets/matrix/matrix.d.ts +2 -4
  49. package/dist/widgets/measurer/measurer.d.ts +0 -1
  50. package/dist/widgets/mock-widgets/mock-widget-types.d.ts +0 -6
  51. package/dist/widgets/mock-widgets/mock-widget.d.ts +2 -5
  52. package/dist/widgets/molecule/molecule.d.ts +0 -1
  53. package/dist/widgets/number-line/number-line.d.ts +2 -3
  54. package/dist/widgets/numeric-input/numeric-input.d.ts +2 -3
  55. package/dist/widgets/orderer/orderer.d.ts +0 -3
  56. package/dist/widgets/passage/passage.d.ts +0 -1
  57. package/dist/widgets/passage-ref/passage-ref.d.ts +0 -2
  58. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +0 -2
  59. package/dist/widgets/plotter/plotter.d.ts +2 -3
  60. package/dist/widgets/radio/radio.d.ts +2 -4
  61. package/dist/widgets/sorter/sorter.d.ts +2 -4
  62. package/dist/widgets/table/table.d.ts +2 -3
  63. package/dist/widgets/video/video.d.ts +0 -3
  64. package/dist/widgets.d.ts +1 -32
  65. package/package.json +46 -46
  66. package/dist/renderer-util.d.ts +0 -14
  67. package/dist/widgets/__shared__/score-noop.d.ts +0 -11
  68. package/dist/widgets/group/score-group.d.ts +0 -4
  69. package/dist/widgets/group/validate-group.d.ts +0 -4
  70. package/dist/widgets/interactive-graphs/mafs-supported-graph-types.d.ts +0 -4
  71. package/dist/widgets/mock-widgets/score-mock-widget.d.ts +0 -5
  72. package/dist/widgets/mock-widgets/validate-mock-widget.d.ts +0 -4
package/dist/index.js CHANGED
@@ -6,7 +6,6 @@ var KAS = require('@khanacademy/kas');
6
6
  var mathInput = require('@khanacademy/math-input');
7
7
  var perseusCore = require('@khanacademy/perseus-core');
8
8
  var PerseusLinter = require('@khanacademy/perseus-linter');
9
- var perseusScore = require('@khanacademy/perseus-score');
10
9
  var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
11
10
  var Tooltip$1 = require('@khanacademy/wonder-blocks-tooltip');
12
11
  var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
@@ -21,6 +20,7 @@ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
21
20
  var $ = require('jquery');
22
21
  var uuid = require('uuid');
23
22
  var PropTypes = require('prop-types');
23
+ var perseusScore = require('@khanacademy/perseus-score');
24
24
  var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
25
25
  var wonderBlocksProgressSpinner = require('@khanacademy/wonder-blocks-progress-spinner');
26
26
  var kmath = require('@khanacademy/kmath');
@@ -33,7 +33,7 @@ var Banner = require('@khanacademy/wonder-blocks-banner');
33
33
  var wonderBlocksDropdown = require('@khanacademy/wonder-blocks-dropdown');
34
34
  var caretDown = require('@phosphor-icons/core/regular/caret-down.svg');
35
35
  var caretUp = require('@phosphor-icons/core/regular/caret-up.svg');
36
- var debounce$1 = require('lodash.debounce');
36
+ require('lodash.debounce');
37
37
  var mafs = require('mafs');
38
38
  var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
39
39
  var questionIcon = require('@phosphor-icons/core/regular/question.svg');
@@ -82,7 +82,6 @@ var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
82
82
  var Banner__default = /*#__PURE__*/_interopDefaultLegacy(Banner);
83
83
  var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
84
84
  var caretUp__default = /*#__PURE__*/_interopDefaultLegacy(caretUp);
85
- var debounce__default = /*#__PURE__*/_interopDefaultLegacy(debounce$1);
86
85
  var questionIcon__default = /*#__PURE__*/_interopDefaultLegacy(questionIcon);
87
86
  var Switch__default = /*#__PURE__*/_interopDefaultLegacy(Switch);
88
87
  var Pill__default = /*#__PURE__*/_interopDefaultLegacy(Pill);
@@ -379,7 +378,7 @@ const mockStrings = {
379
378
  x,
380
379
  y
381
380
  } = _ref27;
382
- return `Point ${num} at ${x} comma ${y}`;
381
+ return `Point ${num} at ${x} comma ${y}.`;
383
382
  },
384
383
  srInteractiveElements: _ref28 => {
385
384
  let {
@@ -670,7 +669,50 @@ const mockStrings = {
670
669
  point3Y
671
670
  } = _ref61;
672
671
  return `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`;
673
- }
672
+ },
673
+ srPolygonGraph: "A polygon.",
674
+ srPolygonGraphCoordinatePlane: "A polygon on a coordinate plane.",
675
+ srPolygonGraphPointsNum: _ref62 => {
676
+ let {
677
+ num
678
+ } = _ref62;
679
+ return `The polygon has ${num} points.`;
680
+ },
681
+ srPolygonGraphPointsOne: "The polygon has 1 point.",
682
+ srPolygonElementsNum: _ref63 => {
683
+ let {
684
+ num
685
+ } = _ref63;
686
+ return `A polygon with ${num} points.`;
687
+ },
688
+ srPolygonElementsOne: "A polygon with 1 point.",
689
+ srPolygonPointAngleApprox: _ref64 => {
690
+ let {
691
+ angle
692
+ } = _ref64;
693
+ return `Angle approximately equal to ${angle} degrees.`;
694
+ },
695
+ srPolygonPointAngle: _ref65 => {
696
+ let {
697
+ angle
698
+ } = _ref65;
699
+ return `Angle equal to ${angle} degrees.`;
700
+ },
701
+ srPolygonSideLength: _ref66 => {
702
+ let {
703
+ pointNum,
704
+ length
705
+ } = _ref66;
706
+ return `A line segment, length equal to ${length} units, connects to point ${pointNum}.`;
707
+ },
708
+ srPolygonSideLengthApprox: _ref67 => {
709
+ let {
710
+ pointNum,
711
+ length
712
+ } = _ref67;
713
+ return `A line segment, length approximately equal to ${length} units, connects to point ${pointNum}.`;
714
+ },
715
+ srUnlimitedPolygonEmpty: "An empty coordinate plane."
674
716
  // The above strings are used for interactive graph SR descriptions.
675
717
  };
676
718
 
@@ -1612,7 +1654,6 @@ const ClassNames = {
1612
1654
  SELECTED: "perseus-radio-selected",
1613
1655
  OPTION_CONTENT: "perseus-radio-option-content"
1614
1656
  },
1615
- INTERACTIVE: "perseus-interactive",
1616
1657
  CORRECT: "perseus-correct",
1617
1658
  INCORRECT: "perseus-incorrect",
1618
1659
  UNANSWERED: "perseus-unanswered",
@@ -1868,7 +1909,6 @@ class Expression extends React__namespace.Component {
1868
1909
  // eslint-disable-next-line react/no-string-refs
1869
1910
  , {
1870
1911
  ref: "input",
1871
- className: ClassNames.INTERACTIVE,
1872
1912
  value: this.props.value,
1873
1913
  onChange: this.changeAndTrack,
1874
1914
  convertDotToTimes: this.props.times,
@@ -1970,7 +2010,6 @@ var Expression$1 = {
1970
2010
  name: "expression",
1971
2011
  displayName: "Expression / Equation",
1972
2012
  accessible: true,
1973
- defaultAlignment: "inline-block",
1974
2013
  widget: ExpressionWithDependencies,
1975
2014
  transform: widgetOptions => {
1976
2015
  const {
@@ -1995,12 +2034,6 @@ var Expression$1 = {
1995
2034
  propUpgrades: perseusCore.expressionLogic.widgetOptionsUpgrades,
1996
2035
  // For use by the editor
1997
2036
  isLintable: true,
1998
- // TODO(LEMS-2656): remove TS suppression
1999
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2000
- scorer: perseusScore.scoreExpression,
2001
- // TODO(LEMS-2656): remove TS suppression
2002
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2003
- validator: perseusScore.validateExpression,
2004
2037
  getPublicWidgetOptions: perseusCore.getExpressionPublicWidgetOptions,
2005
2038
  // TODO(LEMS-2656): remove TS suppression
2006
2039
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
@@ -3280,10 +3313,6 @@ const pureMdMax$1 = "1023px";
3280
3313
  const pureLgMax$1 = "1279px";
3281
3314
  // @tableBackgroundAccent: #f9f9f9; // for striping
3282
3315
  const tableBackgroundAccent = "#F9F9F9";
3283
- // @zIndexAboveScratchpad: @zIndexScratchPad + 1;
3284
- const zIndexAboveScratchpad$1 = 2;
3285
- // @zIndexInteractiveComponent: @zIndexAboveScratchpad + 1;
3286
- const zIndexInteractiveComponent$1 = 3;
3287
3316
  // @phoneMargin: 16px;
3288
3317
  const phoneMargin = 16;
3289
3318
  const negativePhoneMargin = -16;
@@ -3327,8 +3356,6 @@ var constants = /*#__PURE__*/Object.freeze({
3327
3356
  pureMdMax: pureMdMax$1,
3328
3357
  pureLgMax: pureLgMax$1,
3329
3358
  tableBackgroundAccent: tableBackgroundAccent,
3330
- zIndexAboveScratchpad: zIndexAboveScratchpad$1,
3331
- zIndexInteractiveComponent: zIndexInteractiveComponent$1,
3332
3359
  phoneMargin: phoneMargin,
3333
3360
  negativePhoneMargin: negativePhoneMargin,
3334
3361
  hintBorderWidth: hintBorderWidth,
@@ -10074,7 +10101,7 @@ class WrappedLine extends WrappedDrawing {
10074
10101
 
10075
10102
  /* eslint-disable @babel/no-invalid-this */
10076
10103
  const {
10077
- clockwise: clockwise$3,
10104
+ clockwise: clockwise$2,
10078
10105
  reverseVector: reverseVector$1
10079
10106
  } = kmath.geometry;
10080
10107
  function scaledDistanceFromAngle(angle) {
@@ -11312,7 +11339,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11312
11339
  // Temporary objects
11313
11340
  ___default["default"].invoke(polygon.temp, "remove");
11314
11341
  polygon.temp = [];
11315
- const isClockwise = clockwise$3(polygon.coords);
11342
+ const isClockwise = clockwise$2(polygon.coords);
11316
11343
 
11317
11344
  // Update angle labels
11318
11345
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -14745,7 +14772,7 @@ function sum(array) {
14745
14772
  return memo + arg;
14746
14773
  }, 0);
14747
14774
  }
14748
- function clockwise$2(points) {
14775
+ function clockwise$1(points) {
14749
14776
  const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
14750
14777
  const areas = ___default["default"].map(segments, function (segment) {
14751
14778
  const p1 = segment[0];
@@ -14811,7 +14838,7 @@ const draw$1 = {
14811
14838
  const graphie = this.graphie;
14812
14839
  const self = this;
14813
14840
  const coords = ___default["default"].invoke(state.points, "coord");
14814
- const isClockwise = clockwise$2(coords);
14841
+ const isClockwise = clockwise$1(coords);
14815
14842
  const n = coords.length;
14816
14843
 
14817
14844
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -16557,9 +16584,28 @@ class Zoomable extends React__namespace.Component {
16557
16584
  if (!this._isMounted) {
16558
16585
  return;
16559
16586
  }
16587
+
16588
+ // When a zoomable item is contained within a collapsed parent (such as an Explanation widget),
16589
+ // the parent node has zero width, which causes the scale to be miscalculated.
16590
+ // Therefore, we check to see if the node has a valid width, and if it doesn't,
16591
+ // then we traverse up the DOM tree and look for the closest "perseus-renderer"
16592
+ // (which should have a width).
16593
+ // To be cautious, we also account for the possibility that we can't find any parents with a valid width
16594
+ // (not that they don't exist, just that we can't find them).
16595
+ let parentNode = this._node;
16596
+ let currentNode = parentNode;
16597
+ while (currentNode && currentNode.offsetWidth === 0) {
16598
+ // When traversing up the DOM tree, we need to start at the "parentElement"
16599
+ // because "closest()" can match the current element,
16600
+ // and if it does so, then we are stuck in an infinite loop.
16601
+ currentNode = currentNode.parentElement?.closest(".perseus-renderer") ?? null;
16602
+ }
16603
+ if (currentNode) {
16604
+ parentNode = currentNode;
16605
+ }
16560
16606
  const parentBounds = {
16561
- width: this._node.offsetWidth,
16562
- height: this._node.offsetHeight
16607
+ width: parentNode.offsetWidth,
16608
+ height: parentNode.offsetHeight
16563
16609
  };
16564
16610
  const childBounds = this.props.computeChildBounds(this._node, parentBounds);
16565
16611
 
@@ -17623,130 +17669,162 @@ class QuestionParagraph extends React__namespace.Component {
17623
17669
  }
17624
17670
  }
17625
17671
 
17626
- const noScore = {
17627
- type: "points",
17628
- earned: 0,
17629
- total: 0,
17630
- message: null
17631
- };
17632
-
17633
17672
  /**
17634
- * If a widget says that it is empty once it is graded.
17635
- * Trying to encapsulate references to the score format.
17636
- */
17637
- function scoreIsEmpty(score) {
17638
- // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
17639
- // overload the type "invalid" for what should probably be three
17640
- // distinct cases:
17641
- // - truly empty or not fully filled out
17642
- // - invalid or malformed inputs
17643
- // - "almost correct" like inputs where the widget wants to give
17644
- // feedback (e.g. a fraction needs to be reduced, or `pi` should
17645
- // be used instead of 3.14)
17646
- //
17647
- // Unfortunately the coercion happens all over the place, as these
17648
- // Perseus style score objects are created *everywhere* (basically
17649
- // in every widget), so it's hard to change now. We assume that
17650
- // anything with a "message" is not truly empty, and one of the
17651
- // latter two cases for now.
17652
- return score.type === "invalid" && (!score.message || score.message.length === 0);
17653
- }
17654
-
17655
- /**
17656
- * Combine two score objects.
17673
+ * TranslationLinter asynchronously calls Khan Academy's poentry linter.
17657
17674
  *
17658
- * Given two score objects for two different widgets, combine them so that
17659
- * if one is wrong, the total score is wrong, etc.
17675
+ * The poentry linter checks for issues that prevent content from being
17676
+ * translated. This linter is KA-specific and so TranslationLinter does not do
17677
+ * anything in non-KA environments.
17660
17678
  */
17661
- function combineScores(scoreA, scoreB) {
17662
- let message;
17663
- if (scoreA.type === "points" && scoreB.type === "points") {
17664
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
17665
- // TODO(alpert): Figure out how to combine messages usefully
17666
- message = null;
17667
- } else {
17668
- message = scoreA.message || scoreB.message;
17679
+
17680
+ // A LintCB is called once the linter has been run.
17681
+
17682
+ // msec to wait before we actually call the linter after receiving the
17683
+ // last call. The timer is reset if the lint is called during the wait.
17684
+ const DEBOUNCE_TIMEOUT = 1000;
17685
+ class TranslationLinter {
17686
+ // To avoid linting when nothing has changed.
17687
+ previousContent = null;
17688
+
17689
+ /**
17690
+ * Runs the poentry linter on the given perseus string asynchronously.
17691
+ *
17692
+ * It doesn't do anything unless we're in a KA environment.
17693
+ *
17694
+ * onLintErrorsGenerated is called even if there are no errors. It is not
17695
+ * called if perseusStr is unchanged from the last call.
17696
+ *
17697
+ * Linting is relatively expensive -- so we debounce linting each instance
17698
+ * for some amount of time.
17699
+ */
17700
+ runLinter = _.debounce((perseusStr, onLintErrorsGenerated) => {
17701
+ // TODO(FEI-5003): Do we still need to check for 'KA'?
17702
+ if (typeof KA === "undefined") {
17703
+ return;
17669
17704
  }
17670
- return {
17671
- type: "points",
17672
- earned: scoreA.earned + scoreB.earned,
17673
- total: scoreA.total + scoreB.total,
17674
- message: message
17675
- };
17676
- }
17677
- if (scoreA.type === "points" && scoreB.type === "invalid") {
17678
- return scoreB;
17679
- }
17680
- if (scoreA.type === "invalid" && scoreB.type === "points") {
17681
- return scoreA;
17682
- }
17683
- if (scoreA.type === "invalid" && scoreB.type === "invalid") {
17684
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
17685
- // TODO(alpert): Figure out how to combine messages usefully
17686
- message = null;
17687
- } else {
17688
- message = scoreA.message || scoreB.message;
17705
+ if (perseusStr === this.previousContent) {
17706
+ return;
17689
17707
  }
17690
- return {
17691
- type: "invalid",
17692
- message: message
17693
- };
17694
- }
17708
+ this.previousContent = perseusStr;
17709
+ if (perseusStr === "") {
17710
+ onLintErrorsGenerated([]);
17711
+ return;
17712
+ }
17713
+ fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
17714
+ headers: {
17715
+ "Content-Type": "application/json"
17716
+ },
17717
+ body: JSON.stringify({
17718
+ // The poentry linter verifies that the translation from
17719
+ // the original text to the translated text is sane. We
17720
+ // want to ensure that the translation from English to
17721
+ // English is sane, so msgid === mstr.
17722
+ msgid: perseusStr,
17723
+ // msgid is the original text
17724
+ msgstr: perseusStr,
17725
+ // msgstr is the translated text
17726
+ format: "perseus_text",
17727
+ filename: ""
17728
+ }),
17729
+ method: "POST"
17730
+ }).then(response => {
17731
+ if (response.status >= 400) {
17732
+ return {
17733
+ status: "error",
17734
+ message: "Could not run i18n linter."
17735
+ };
17736
+ }
17737
+ return response.json();
17738
+ }, rejection => {
17739
+ return {
17740
+ status: "error",
17741
+ message: "Could not run i18n linter."
17742
+ };
17743
+ }).then(json => {
17744
+ if (json.status === "error") {
17745
+ onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
17746
+ } else {
17747
+ onLintErrorsGenerated([]);
17748
+ }
17749
+ });
17750
+ }, DEBOUNCE_TIMEOUT);
17695
17751
 
17696
17752
  /**
17697
- * The above checks cover all combinations of score type, so if we get here
17698
- * then something is amiss with our inputs.
17753
+ * Applies an array of errors generated by linters without position
17754
+ * information (TranslationLinter and the legacy getSaveWarnings()) to the
17755
+ * top of a Perseus tree.
17699
17756
  */
17700
- throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
17701
- metadata: {
17702
- scoreA: JSON.stringify(scoreA),
17703
- scoreB: JSON.stringify(scoreB)
17757
+ applyLintErrors(parsedMarkdown, translationLintErrors) {
17758
+ // These lint errors do not have position data associated with
17759
+ // them, so we just plop them at the top.
17760
+ if (translationLintErrors.length) {
17761
+ const errorText = translationLintErrors.join("\n\n");
17762
+ parsedMarkdown.unshift({
17763
+ content: {
17764
+ type: "text",
17765
+ content: ""
17766
+ },
17767
+ insideTable: false,
17768
+ message: errorText,
17769
+ ruleName: "legacy-error",
17770
+ severity: PerseusLinter.Rule.Severity.ERROR,
17771
+ type: "lint"
17772
+ });
17704
17773
  }
17705
- });
17706
- }
17707
- function flattenScores(widgetScoreMap) {
17708
- return Object.values(widgetScoreMap).reduce(combineScores, noScore);
17709
- }
17710
- function isCorrect(score) {
17711
- return score.type === "points" && score.earned >= score.total;
17774
+ }
17712
17775
  }
17713
- function keScoreFromPerseusScore(score,
17714
- // It's weird, but this is what we're passing it
17715
- guess, state) {
17716
- if (score.type === "points") {
17717
- return {
17718
- empty: false,
17719
- correct: isCorrect(score),
17720
- message: score.message,
17721
- guess: guess,
17722
- state: state
17723
- };
17776
+
17777
+ /**
17778
+ * Preprocess TeX code to convert things that MathJax doesn't know how to handle
17779
+ * to things is does.
17780
+ */
17781
+ var preprocessTex = (texCode => texCode
17782
+ // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
17783
+ // support (yet) with \begin{aligned}...\end{aligned} which renders
17784
+ // the same is supported by KaTeX. It does the same for align*.
17785
+ // TODO(kevinb) update content to use aligned instead of align.
17786
+ // TODO(LEMS-1608) Remove this replacement as MathJax supports the
17787
+ // "align" macro correctly (and, in fact, it is not synonymous with
17788
+ // "aligned").
17789
+ .replace(/\{align[*]?\}/g, "{aligned}")
17790
+ // Replace non-breaking spaces with regular spaces.
17791
+ .replace(/[\u00a0]/g, " "));
17792
+
17793
+ const {
17794
+ interactiveSizes
17795
+ } = constants;
17796
+
17797
+ // Note: these size cutoffs represent content-width cutoffs as specified in
17798
+ // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
17799
+ // TODO(benkomalo): these values aren't used in JS outside of this file, but
17800
+ // are coupled to the values in styles/styles.less - DRY it up at some point
17801
+ const smMax = articleMaxWidthTableInPx;
17802
+ const mdMax = articleMaxWidthInPx;
17803
+ const containerSizeClass = {
17804
+ SMALL: "small",
17805
+ MEDIUM: "medium",
17806
+ LARGE: "large",
17807
+ XLARGE: "xlarge"
17808
+ };
17809
+ const getClassFromWidth = width => {
17810
+ if (!width) {
17811
+ return containerSizeClass.MEDIUM;
17724
17812
  }
17725
- if (score.type === "invalid") {
17726
- return {
17727
- empty: true,
17728
- correct: false,
17729
- message: score.message,
17730
- suppressAlmostThere: score.suppressAlmostThere,
17731
- guess: guess,
17732
- state: state
17733
- };
17813
+ if (width <= smMax) {
17814
+ return containerSizeClass.SMALL;
17734
17815
  }
17735
- throw new perseusCore.PerseusError(
17736
- // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
17737
- "Invalid score type: " + score.type, perseusCore.Errors.InvalidInput, {
17738
- metadata: {
17739
- score: JSON.stringify(score),
17740
- guess: JSON.stringify(guess),
17741
- state: JSON.stringify(state)
17742
- }
17743
- });
17744
- }
17816
+ if (width <= mdMax) {
17817
+ return containerSizeClass.MEDIUM;
17818
+ }
17819
+ return containerSizeClass.LARGE;
17820
+ };
17821
+ const getInteractiveBoxFromSizeClass = sizeClass => {
17822
+ if (sizeClass === containerSizeClass.SMALL) {
17823
+ return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
17824
+ }
17825
+ return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
17826
+ };
17745
17827
 
17746
- const DEFAULT_ALIGNMENT$1 = "block";
17747
- // NOTE(kevinb): "default" is not one in `validAlignments`.
17748
- const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
17749
- const DEFAULT_STATIC = false;
17750
17828
  const DEFAULT_TRACKING = "";
17751
17829
  const DEFAULT_LINTABLE = false;
17752
17830
  const widgets = {};
@@ -17763,7 +17841,6 @@ const registerWidgets = widgets => {
17763
17841
  widgets.forEach(widget => {
17764
17842
  registerWidget(widget.name, widget);
17765
17843
  });
17766
- validateAlignments();
17767
17844
  };
17768
17845
 
17769
17846
  /**
@@ -17844,12 +17921,6 @@ const getWidget = name => {
17844
17921
  const getWidgetExport = name => {
17845
17922
  return widgets[name] ?? null;
17846
17923
  };
17847
- const getWidgetValidator = name => {
17848
- return widgets[name]?.validator ?? null;
17849
- };
17850
- const getWidgetScorer = name => {
17851
- return widgets[name]?.scorer ?? null;
17852
- };
17853
17924
  const getPublicWidgetOptionsFunction = name => {
17854
17925
  return widgets[name]?.getPublicWidgetOptions ?? (i => i);
17855
17926
  };
@@ -17895,112 +17966,6 @@ const isAccessible = widgetInfo => {
17895
17966
  const getAllWidgetTypes = () => {
17896
17967
  return ___default["default"].keys(widgets);
17897
17968
  };
17898
- const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
17899
- const type = oldWidgetInfo.type;
17900
- // NOTE(jeremy): This looks like it could be replaced by fixing types so
17901
- // that `type` is non-optional. But we're seeing this in Sentry today so I
17902
- // suspect we have legacy data (potentially unpublished) and we should
17903
- // figure that out before depending solely on types.
17904
- if (!___default["default"].isString(type)) {
17905
- throw new perseusCore.PerseusError("widget type must be a string, but was: " + type, perseusCore.Errors.Internal);
17906
- }
17907
- const widgetExports = widgets[type];
17908
- if (widgetExports == null) {
17909
- // If we have a widget that isn't registered, we can't upgrade it
17910
- // TODO(aria): Figure out what the best thing to do here would be
17911
- return oldWidgetInfo;
17912
- }
17913
-
17914
- // Unversioned widgets (pre-July 2014) are all implicitly 0.0
17915
- const initialVersion = oldWidgetInfo.version || {
17916
- major: 0,
17917
- minor: 0
17918
- };
17919
- const latestVersion = widgetExports.version || {
17920
- major: 0,
17921
- minor: 0
17922
- };
17923
-
17924
- // If the widget version is later than what we understand (major
17925
- // version is higher than latest, or major versions are equal and minor
17926
- // version is higher than latest), don't perform any upgrades.
17927
- if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
17928
- return oldWidgetInfo;
17929
- }
17930
-
17931
- // We do a clone here so that it's safe to mutate the input parameter
17932
- // in propUpgrades functions (which I will probably accidentally do at
17933
- // some point, and we would like to not break when that happens).
17934
- let newEditorProps = ___default["default"].clone(oldWidgetInfo.options) || {};
17935
- const upgradePropsMap = widgetExports.propUpgrades || {};
17936
-
17937
- // Empty props usually mean a newly created widget by the editor,
17938
- // and are always considerered up-to-date.
17939
- // Mostly, we'd rather not run upgrade functions on props that are
17940
- // not complete.
17941
- if (___default["default"].keys(newEditorProps).length !== 0) {
17942
- // We loop through all the versions after the current version of
17943
- // the loaded widget, up to and including the latest version of the
17944
- // loaded widget, and run the upgrade function to bring our loaded
17945
- // widget's props up to that version.
17946
- // There is a little subtlety here in that we call
17947
- // upgradePropsMap[1] to upgrade *to* version 1,
17948
- // (not from version 1).
17949
- for (let nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
17950
- if (upgradePropsMap[String(nextVersion)]) {
17951
- newEditorProps = upgradePropsMap[String(nextVersion)](newEditorProps);
17952
- } else {
17953
- // This is a Log.error because it is unlikely to be hit in
17954
- // local testing, and a Log.error is slightly less scary in
17955
- // prod than a `throw new Error`
17956
- Log.error("No upgrade found for widget. Cannot render.", perseusCore.Errors.Internal, {
17957
- loggedMetadata: {
17958
- type,
17959
- fromMajorVersion: nextVersion - 1,
17960
- toMajorVersion: nextVersion
17961
- }
17962
- });
17963
- // But try to keep going anyways (yolo!)
17964
- // (Throwing an error here would just break the page
17965
- // silently anyways, so that doesn't seem much better
17966
- // than a halfhearted attempt to continue, however
17967
- // shallow...)
17968
- }
17969
- }
17970
- }
17971
-
17972
- // Minor version upgrades (eg. new optional props) don't have
17973
- // transform functions. Instead, we fill in the new props with their
17974
- // defaults.
17975
- const defaultProps = type in editors ? editors[type].defaultProps : {};
17976
- newEditorProps = {
17977
- ...defaultProps,
17978
- ...newEditorProps
17979
- };
17980
- let alignment = oldWidgetInfo.alignment;
17981
-
17982
- // Widgets that support multiple alignments will "lock in" the
17983
- // alignment to the alignment that would be listed first in the
17984
- // select box. If the widget only supports one alignment, the
17985
- // alignment value will likely just end up as "default".
17986
- if (alignment == null || alignment === "default") {
17987
- alignment = getSupportedAlignments(type)[0];
17988
- }
17989
- let widgetStatic = oldWidgetInfo.static;
17990
- if (widgetStatic == null) {
17991
- widgetStatic = DEFAULT_STATIC;
17992
- }
17993
- return ___default["default"].extend({}, oldWidgetInfo, {
17994
- // maintain other info, like type
17995
- // After upgrading we guarantee that the version is up-to-date
17996
- version: latestVersion,
17997
- // Default graded to true (so null/undefined becomes true):
17998
- graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
17999
- alignment: alignment,
18000
- static: widgetStatic,
18001
- options: newEditorProps
18002
- });
18003
- };
18004
17969
  const getRendererPropsForWidgetInfo = (widgetInfo, strings, problemNum) => {
18005
17970
  const type = widgetInfo.type;
18006
17971
  const widgetExports = widgets[type];
@@ -18042,70 +18007,6 @@ const traverseChildWidgets$3 = (widgetInfo, traverseRenderer) => {
18042
18007
  return widgetInfo;
18043
18008
  };
18044
18009
 
18045
- /**
18046
- * Handling for the optional alignments for widgets
18047
- * See widget-container.jsx for details on how alignments are implemented.
18048
- */
18049
-
18050
- /**
18051
- * Returns the list of supported alignments for the given (string) widget
18052
- * type. This is used primarily at editing time to display the choices
18053
- * for the user.
18054
- *
18055
- * Supported alignments are given as an array of strings in the exports of
18056
- * a widget's module.
18057
- */
18058
- const getSupportedAlignments = type => {
18059
- const widgetExport = widgets[type];
18060
- // @ts-expect-error - TS2322 - Type 'string[] | readonly Alignment[]' is not assignable to type 'readonly Alignment[]'.
18061
- return widgetExport && widgetExport.supportedAlignments || DEFAULT_SUPPORTED_ALIGNMENTS;
18062
- };
18063
-
18064
- /**
18065
- * For the given (string) widget type, determine the default alignment for
18066
- * the widget. This is used at rendering time to go from "default" alignment
18067
- * to the actual alignment displayed on the screen.
18068
- *
18069
- * The default alignment is given either as a string (called
18070
- * `defaultAlignment`) or a function (called `getDefaultAlignment`) on
18071
- * the exports of a widget's module.
18072
- */
18073
- const getDefaultAlignment = type => {
18074
- const widgetExports = widgets[type];
18075
- let alignment;
18076
- if (!widgetExports) {
18077
- return DEFAULT_ALIGNMENT$1;
18078
- }
18079
- if (widgetExports.getDefaultAlignment) {
18080
- alignment = widgetExports.getDefaultAlignment();
18081
- } else {
18082
- alignment = widgetExports.defaultAlignment;
18083
- }
18084
- return alignment || DEFAULT_ALIGNMENT$1;
18085
- };
18086
- const validAlignments = ["block", "inline-block", "inline", "float-left", "float-right", "full-width"];
18087
-
18088
- /**
18089
- * Used at startup to fail fast if an alignment given by a widget is
18090
- * invalid.
18091
- */
18092
- // TODO(alex): Change this to run as a testcase (vs. being run at runtime)
18093
- // TODO(LP-10707): I think this can be completely removed because our TypeScript types
18094
- // enforce this!
18095
- const validateAlignments = () => {
18096
- ___default["default"].each(widgets, function (widgetInfo) {
18097
- if (widgetInfo.defaultAlignment && !___default["default"].contains(validAlignments, widgetInfo.defaultAlignment)) {
18098
- throw new perseusCore.PerseusError("Widget '" + widgetInfo.displayName + "' has an invalid defaultAlignment value: " + widgetInfo.defaultAlignment, perseusCore.Errors.InvalidInput);
18099
- }
18100
- if (widgetInfo.supportedAlignments) {
18101
- const unknownAlignments = ___default["default"].difference(widgetInfo.supportedAlignments, validAlignments);
18102
- if (unknownAlignments.length) {
18103
- throw new perseusCore.PerseusError("Widget '" + widgetInfo.displayName + "' has an invalid value for supportedAlignments: " + unknownAlignments.join(" "), perseusCore.Errors.InvalidInput);
18104
- }
18105
- }
18106
- });
18107
- };
18108
-
18109
18010
  /**
18110
18011
  * Handling for static mode for widgets that support it.
18111
18012
  */
@@ -18159,8 +18060,6 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18159
18060
  replaceDeprecatedEditors: replaceDeprecatedEditors,
18160
18061
  getWidget: getWidget,
18161
18062
  getWidgetExport: getWidgetExport,
18162
- getWidgetValidator: getWidgetValidator,
18163
- getWidgetScorer: getWidgetScorer,
18164
18063
  getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
18165
18064
  getEditor: getEditor,
18166
18065
  getTransform: getTransform,
@@ -18169,267 +18068,14 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18169
18068
  getPublicWidgets: getPublicWidgets,
18170
18069
  isAccessible: isAccessible,
18171
18070
  getAllWidgetTypes: getAllWidgetTypes,
18172
- upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
18173
18071
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
18174
18072
  traverseChildWidgets: traverseChildWidgets$3,
18175
- getSupportedAlignments: getSupportedAlignments,
18176
- getDefaultAlignment: getDefaultAlignment,
18177
- validateAlignments: validateAlignments,
18178
18073
  supportsStaticMode: supportsStaticMode,
18179
18074
  getStaticTransform: getStaticTransform,
18180
18075
  getTracking: getTracking,
18181
18076
  isLintable: isLintable
18182
18077
  });
18183
18078
 
18184
- function getUpgradedWidgetOptions(oldWidgetOptions) {
18185
- return perseusCore.mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
18186
- if (!widgetInfo.type || !widgetInfo.alignment) {
18187
- const newValues = {};
18188
- if (!widgetInfo.type) {
18189
- // TODO: why does widget have no type?
18190
- // We don't want to derive type from widget ID
18191
- // see: LEMS-1845
18192
- newValues.type = widgetId.split(" ")[0];
18193
- }
18194
- if (!widgetInfo.alignment) {
18195
- newValues.alignment = "default";
18196
- }
18197
- widgetInfo = {
18198
- ...widgetInfo,
18199
- ...newValues
18200
- };
18201
- }
18202
- // TODO(LEMS-2656): remove TS suppression
18203
- return upgradeWidgetInfoToLatestVersion(widgetInfo);
18204
- });
18205
- }
18206
-
18207
- /**
18208
- * Checks the given user input to see if any answerable widgets have not been
18209
- * "filled in" (ie. if they're empty). Another way to think about this
18210
- * function is that its a check to see if we can score the provided input.
18211
- */
18212
- function emptyWidgetsFunctional(widgets,
18213
- // This is a port of old code, I'm not sure why
18214
- // we need widgetIds vs the keys of the widgets object
18215
- widgetIds, userInputMap, strings, locale) {
18216
- return widgetIds.filter(id => {
18217
- const widget = widgets[id];
18218
- if (!widget || widget.static === true) {
18219
- // Static widgets shouldn't count as empty
18220
- return false;
18221
- }
18222
- const validator = getWidgetValidator(widget.type);
18223
- const userInput = userInputMap[id];
18224
- const validationData = widget.options;
18225
- const score = validator?.(userInput, validationData, strings, locale);
18226
- if (score) {
18227
- return scoreIsEmpty(score);
18228
- }
18229
- });
18230
- }
18231
-
18232
- // TODO: combine scorePerseusItem with scoreWidgetsFunctional
18233
- // once scorePerseusItem is the only one calling scoreWidgetsFunctional
18234
- function scorePerseusItem(perseusRenderData, userInputMap,
18235
- // TODO(LEMS-2461,LEMS-2391): these probably
18236
- // need to be removed before we move this to the server
18237
- strings, locale) {
18238
- // There seems to be a chance that PerseusRenderer.widgets might include
18239
- // widget data for widgets that are not in PerseusRenderer.content,
18240
- // so this checks that the widgets are being used before scoring them
18241
- const usedWidgetIds = perseusCore.getWidgetIdsFromContent(perseusRenderData.content);
18242
- const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
18243
- return flattenScores(scores);
18244
- }
18245
- function scoreWidgetsFunctional(widgets,
18246
- // This is a port of old code, I'm not sure why
18247
- // we need widgetIds vs the keys of the widgets object
18248
- widgetIds, userInputMap, strings, locale) {
18249
- const upgradedWidgets = getUpgradedWidgetOptions(widgets);
18250
- const gradedWidgetIds = widgetIds.filter(id => {
18251
- const props = upgradedWidgets[id];
18252
- const widgetIsGraded = props?.graded == null || props.graded;
18253
- const widgetIsStatic = !!props?.static;
18254
- // Ungraded widgets or widgets set to static shouldn't be graded.
18255
- return widgetIsGraded && !widgetIsStatic;
18256
- });
18257
- const widgetScores = {};
18258
- gradedWidgetIds.forEach(id => {
18259
- const widget = upgradedWidgets[id];
18260
- if (!widget) {
18261
- return;
18262
- }
18263
- const userInput = userInputMap[id];
18264
- const validator = getWidgetValidator(widget.type);
18265
- const scorer = getWidgetScorer(widget.type);
18266
-
18267
- // We do validation (empty checks) first and then scoring. If
18268
- // validation fails, it's result is itself a PerseusScore.
18269
- const score = validator?.(userInput, widget.options, strings, locale) ?? scorer?.(userInput, widget.options, strings, locale);
18270
- if (score != null) {
18271
- widgetScores[id] = score;
18272
- }
18273
- });
18274
- return widgetScores;
18275
- }
18276
-
18277
- /**
18278
- * TranslationLinter asynchronously calls Khan Academy's poentry linter.
18279
- *
18280
- * The poentry linter checks for issues that prevent content from being
18281
- * translated. This linter is KA-specific and so TranslationLinter does not do
18282
- * anything in non-KA environments.
18283
- */
18284
-
18285
- // A LintCB is called once the linter has been run.
18286
-
18287
- // msec to wait before we actually call the linter after receiving the
18288
- // last call. The timer is reset if the lint is called during the wait.
18289
- const DEBOUNCE_TIMEOUT = 1000;
18290
- class TranslationLinter {
18291
- // To avoid linting when nothing has changed.
18292
- previousContent = null;
18293
-
18294
- /**
18295
- * Runs the poentry linter on the given perseus string asynchronously.
18296
- *
18297
- * It doesn't do anything unless we're in a KA environment.
18298
- *
18299
- * onLintErrorsGenerated is called even if there are no errors. It is not
18300
- * called if perseusStr is unchanged from the last call.
18301
- *
18302
- * Linting is relatively expensive -- so we debounce linting each instance
18303
- * for some amount of time.
18304
- */
18305
- runLinter = _.debounce((perseusStr, onLintErrorsGenerated) => {
18306
- // TODO(FEI-5003): Do we still need to check for 'KA'?
18307
- if (typeof KA === "undefined") {
18308
- return;
18309
- }
18310
- if (perseusStr === this.previousContent) {
18311
- return;
18312
- }
18313
- this.previousContent = perseusStr;
18314
- if (perseusStr === "") {
18315
- onLintErrorsGenerated([]);
18316
- return;
18317
- }
18318
- fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
18319
- headers: {
18320
- "Content-Type": "application/json"
18321
- },
18322
- body: JSON.stringify({
18323
- // The poentry linter verifies that the translation from
18324
- // the original text to the translated text is sane. We
18325
- // want to ensure that the translation from English to
18326
- // English is sane, so msgid === mstr.
18327
- msgid: perseusStr,
18328
- // msgid is the original text
18329
- msgstr: perseusStr,
18330
- // msgstr is the translated text
18331
- format: "perseus_text",
18332
- filename: ""
18333
- }),
18334
- method: "POST"
18335
- }).then(response => {
18336
- if (response.status >= 400) {
18337
- return {
18338
- status: "error",
18339
- message: "Could not run i18n linter."
18340
- };
18341
- }
18342
- return response.json();
18343
- }, rejection => {
18344
- return {
18345
- status: "error",
18346
- message: "Could not run i18n linter."
18347
- };
18348
- }).then(json => {
18349
- if (json.status === "error") {
18350
- onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
18351
- } else {
18352
- onLintErrorsGenerated([]);
18353
- }
18354
- });
18355
- }, DEBOUNCE_TIMEOUT);
18356
-
18357
- /**
18358
- * Applies an array of errors generated by linters without position
18359
- * information (TranslationLinter and the legacy getSaveWarnings()) to the
18360
- * top of a Perseus tree.
18361
- */
18362
- applyLintErrors(parsedMarkdown, translationLintErrors) {
18363
- // These lint errors do not have position data associated with
18364
- // them, so we just plop them at the top.
18365
- if (translationLintErrors.length) {
18366
- const errorText = translationLintErrors.join("\n\n");
18367
- parsedMarkdown.unshift({
18368
- content: {
18369
- type: "text",
18370
- content: ""
18371
- },
18372
- insideTable: false,
18373
- message: errorText,
18374
- ruleName: "legacy-error",
18375
- severity: PerseusLinter.Rule.Severity.ERROR,
18376
- type: "lint"
18377
- });
18378
- }
18379
- }
18380
- }
18381
-
18382
- /**
18383
- * Preprocess TeX code to convert things that MathJax doesn't know how to handle
18384
- * to things is does.
18385
- */
18386
- var preprocessTex = (texCode => texCode
18387
- // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
18388
- // support (yet) with \begin{aligned}...\end{aligned} which renders
18389
- // the same is supported by KaTeX. It does the same for align*.
18390
- // TODO(kevinb) update content to use aligned instead of align.
18391
- // TODO(LEMS-1608) Remove this replacement as MathJax supports the
18392
- // "align" macro correctly (and, in fact, it is not synonymous with
18393
- // "aligned").
18394
- .replace(/\{align[*]?\}/g, "{aligned}")
18395
- // Replace non-breaking spaces with regular spaces.
18396
- .replace(/[\u00a0]/g, " "));
18397
-
18398
- const {
18399
- interactiveSizes
18400
- } = constants;
18401
-
18402
- // Note: these size cutoffs represent content-width cutoffs as specified in
18403
- // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
18404
- // TODO(benkomalo): these values aren't used in JS outside of this file, but
18405
- // are coupled to the values in styles/styles.less - DRY it up at some point
18406
- const smMax = articleMaxWidthTableInPx;
18407
- const mdMax = articleMaxWidthInPx;
18408
- const containerSizeClass = {
18409
- SMALL: "small",
18410
- MEDIUM: "medium",
18411
- LARGE: "large",
18412
- XLARGE: "xlarge"
18413
- };
18414
- const getClassFromWidth = width => {
18415
- if (!width) {
18416
- return containerSizeClass.MEDIUM;
18417
- }
18418
- if (width <= smMax) {
18419
- return containerSizeClass.SMALL;
18420
- }
18421
- if (width <= mdMax) {
18422
- return containerSizeClass.MEDIUM;
18423
- }
18424
- return containerSizeClass.LARGE;
18425
- };
18426
- const getInteractiveBoxFromSizeClass = sizeClass => {
18427
- if (sizeClass === containerSizeClass.SMALL) {
18428
- return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
18429
- }
18430
- return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
18431
- };
18432
-
18433
18079
  class WidgetContainer extends React__namespace.Component {
18434
18080
  widgetRef = /*#__PURE__*/React__namespace.createRef();
18435
18081
  static defaultProps = {
@@ -18502,7 +18148,7 @@ class WidgetContainer extends React__namespace.Component {
18502
18148
  }
18503
18149
  let alignment = this.state.widgetProps.alignment;
18504
18150
  if (alignment === "default") {
18505
- alignment = getDefaultAlignment(type);
18151
+ alignment = perseusCore.CoreWidgetRegistry.getDefaultAlignment(type);
18506
18152
  }
18507
18153
  className += " widget-" + alignment;
18508
18154
  const apiOptions = this.state.widgetProps.apiOptions;
@@ -18510,6 +18156,7 @@ class WidgetContainer extends React__namespace.Component {
18510
18156
  // Hack to prevent interaction with static widgets: we overlay a big
18511
18157
  // div on top of the widget and overflow: hidden the container.
18512
18158
  // Ideally widgets themselves should know how to prevent interaction.
18159
+ // UPDATE HTML5: `inert` on the underlying div would be better
18513
18160
  const isStatic = this.state.widgetProps.static || apiOptions.readOnly;
18514
18161
  const staticContainerStyles = {
18515
18162
  position: "relative",
@@ -18521,7 +18168,7 @@ class WidgetContainer extends React__namespace.Component {
18521
18168
  position: "absolute",
18522
18169
  top: 0,
18523
18170
  left: 0,
18524
- zIndex: zIndexInteractiveComponent$1
18171
+ zIndex: 3
18525
18172
  };
18526
18173
 
18527
18174
  // Some widgets may include strings of markdown that we may
@@ -18787,7 +18434,7 @@ class Renderer extends React__namespace.Component {
18787
18434
  };
18788
18435
  };
18789
18436
  _getInitialWidgetState = props => {
18790
- const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
18437
+ const allWidgetInfo = perseusCore.getUpgradedWidgetOptions(props.widgets);
18791
18438
  return {
18792
18439
  widgetInfo: allWidgetInfo,
18793
18440
  widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
@@ -19699,7 +19346,7 @@ class Renderer extends React__namespace.Component {
19699
19346
  * empty if the graph is in the starting state.
19700
19347
  */
19701
19348
  emptyWidgets() {
19702
- return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
19349
+ return perseusScore.emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.context.locale);
19703
19350
  }
19704
19351
  _setWidgetProps = (id, newProps, cb, silent) => {
19705
19352
  this.setState(prevState => {
@@ -19829,8 +19476,8 @@ class Renderer extends React__namespace.Component {
19829
19476
  * @deprecated use scorePerseusItem
19830
19477
  */
19831
19478
  score() {
19832
- const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
19833
- const combinedScore = flattenScores(scores);
19479
+ const scores = perseusScore.scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.context.locale);
19480
+ const combinedScore = perseusScore.flattenScores(scores);
19834
19481
  return combinedScore;
19835
19482
  }
19836
19483
 
@@ -20351,7 +19998,7 @@ class InputWithExamples extends React__namespace.Component {
20351
19998
  };
20352
19999
  _getInputClassName = () => {
20353
20000
  // Otherwise, we need to add these INPUT and FOCUSED tags here.
20354
- let className = ClassNames.INPUT + " " + ClassNames.INTERACTIVE;
20001
+ let className = ClassNames.INPUT;
20355
20002
  if (this.state.focused) {
20356
20003
  className += " " + ClassNames.FOCUSED;
20357
20004
  }
@@ -20744,14 +20391,10 @@ const propTransform$3 = widgetOptions => {
20744
20391
  var InputNumber$1 = {
20745
20392
  name: "input-number",
20746
20393
  displayName: "Input number (deprecated - use numeric input instead)",
20747
- defaultAlignment: "inline-block",
20748
20394
  hidden: true,
20749
20395
  widget: InputNumber,
20750
20396
  transform: propTransform$3,
20751
20397
  isLintable: true,
20752
- // TODO(LEMS-2656): remove TS suppression
20753
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
20754
- scorer: perseusScore.scoreInputNumber,
20755
20398
  getOneCorrectAnswerFromRubric(rubric) {
20756
20399
  if (rubric.value == null) {
20757
20400
  return;
@@ -21003,14 +20646,11 @@ const propsTransform = function (widgetOptions) {
21003
20646
  var NumericInput$1 = {
21004
20647
  name: "numeric-input",
21005
20648
  displayName: "Numeric input",
21006
- defaultAlignment: "inline-block",
21007
20649
  accessible: true,
21008
20650
  widget: NumericInput,
21009
20651
  transform: propsTransform,
21010
20652
  isLintable: true,
21011
- // TODO(LEMS-2656): remove TS suppression
21012
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
21013
- scorer: perseusScore.scoreNumericInput,
20653
+ getPublicWidgetOptions: perseusCore.getNumericInputPublicWidgetOptions,
21014
20654
  // TODO(LEMS-2656): remove TS suppression
21015
20655
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
21016
20656
  getOneCorrectAnswerFromRubric(rubric) {
@@ -21071,24 +20711,6 @@ const getPromptJSON$p = renderProps => {
21071
20711
  };
21072
20712
  };
21073
20713
 
21074
- /**
21075
- * Several widgets don't have "right"/"wrong" scoring logic,
21076
- * so this just says to move on past those widgets
21077
- *
21078
- * TODO(LEMS-2543) widgets that use this probably shouldn't have any
21079
- * scoring logic and the thing scoring an exercise
21080
- * should just know to skip these
21081
- */
21082
- function scoreNoop() {
21083
- let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
21084
- return {
21085
- type: "points",
21086
- earned: points,
21087
- total: points,
21088
- message: null
21089
- };
21090
- }
21091
-
21092
20714
  /**
21093
20715
  * Utility functions for manipulating ranges of highlightable content.
21094
20716
  */
@@ -23266,8 +22888,7 @@ var Passage$1 = {
23266
22888
  transform: editorProps => {
23267
22889
  return ___default["default"].pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
23268
22890
  },
23269
- isLintable: true,
23270
- scorer: () => scoreNoop()
22891
+ isLintable: true
23271
22892
  };
23272
22893
 
23273
22894
  /**
@@ -23393,16 +23014,13 @@ var PassageRef$1 = {
23393
23014
  name: "passage-ref",
23394
23015
  displayName: "PassageRef (SAT only)",
23395
23016
  hidden: true,
23396
- defaultAlignment: "inline",
23397
23017
  widget: PassageRef,
23398
23018
  transform: widgetOptions => ({
23399
23019
  passageNumber: widgetOptions.passageNumber,
23400
23020
  referenceNumber: widgetOptions.referenceNumber,
23401
23021
  summaryText: widgetOptions.summaryText
23402
23022
  }),
23403
- version: perseusCore.passageRefLogic.version,
23404
- // TODO: things that aren't interactive shouldn't need scoring functions
23405
- scorer: () => scoreNoop()
23023
+ version: perseusCore.passageRefLogic.version
23406
23024
  };
23407
23025
 
23408
23026
  /**
@@ -23444,109 +23062,6 @@ var mediaQueries = {
23444
23062
  xlOrLarger: `@media screen and (min-width: ${pureXlMin})`
23445
23063
  };
23446
23064
 
23447
- const {
23448
- zIndexAboveScratchpad,
23449
- zIndexInteractiveComponent,
23450
- radioBorderColor,
23451
- checkedColor,
23452
- circleSize,
23453
- radioMarginWidth
23454
- } = constants;
23455
- var sharedStyles = aphrodite.StyleSheet.create({
23456
- perseusInteractive: {
23457
- zIndex: zIndexInteractiveComponent,
23458
- position: "relative"
23459
- },
23460
- aboveScratchpad: {
23461
- position: "relative",
23462
- zIndex: zIndexAboveScratchpad
23463
- },
23464
- blankBackground: {
23465
- // TODO(emily): Use WB colors?
23466
- backgroundColor: "#FDFDFD"
23467
- },
23468
- perseusSrOnly: {
23469
- border: 0,
23470
- clip: "rect(0,0,0,0)",
23471
- height: 1,
23472
- margin: -1,
23473
- overflow: "hidden",
23474
- padding: 0,
23475
- position: "absolute",
23476
- width: 1
23477
- },
23478
- responsiveLabel: {
23479
- // NOTE(charlie): The values used here should be kept in sync with the
23480
- // caption text sizing in Perseus' styles.less.
23481
- // TODO(charlie): Migrate the captions over to using this style.
23482
- [mediaQueries.smOrSmaller]: {
23483
- fontSize: 14,
23484
- lineHeight: 1.3
23485
- },
23486
- [mediaQueries.md]: {
23487
- fontSize: 17,
23488
- lineHeight: 1.4
23489
- },
23490
- [mediaQueries.lgOrLarger]: {
23491
- fontSize: 20,
23492
- lineHeight: 1.4
23493
- }
23494
- },
23495
- responsiveInput: {
23496
- display: "inline-block",
23497
- WebkitAppearance: "none",
23498
- appearance: "none",
23499
- "::-ms-check": {
23500
- display: "none"
23501
- },
23502
- backgroundColor: "#fff",
23503
- border: "2px solid #fff",
23504
- boxShadow: `0 0px 0px 1px ${radioBorderColor}`,
23505
- outline: "none",
23506
- boxSizing: "border-box",
23507
- flexShrink: 0,
23508
- marginBottom: 1,
23509
- marginLeft: 1,
23510
- marginRight: 1,
23511
- marginTop: 1,
23512
- height: circleSize - 2,
23513
- width: circleSize - 2
23514
- },
23515
- responsiveRadioInput: {
23516
- borderRadius: "50%",
23517
- ":checked": {
23518
- backgroundColor: checkedColor,
23519
- border: "none",
23520
- borderRadius: "50%",
23521
- boxShadow: `inset 0px 0px 0px 2px white, ` + `0 0px 0px 2px ${checkedColor}`,
23522
- marginTop: radioMarginWidth,
23523
- marginBottom: radioMarginWidth,
23524
- marginLeft: radioMarginWidth,
23525
- marginRight: radioMarginWidth,
23526
- height: circleSize - 2 * radioMarginWidth,
23527
- width: circleSize - 2 * radioMarginWidth
23528
- }
23529
- },
23530
- responsiveRadioInputActive: {
23531
- backgroundColor: "#fff",
23532
- border: "2px solid #fff",
23533
- borderRadius: "50%",
23534
- boxShadow: `0 0px 0px 2px ${checkedColor}`,
23535
- marginTop: radioMarginWidth,
23536
- marginBottom: radioMarginWidth,
23537
- marginLeft: radioMarginWidth,
23538
- marginRight: radioMarginWidth,
23539
- height: circleSize - 2 * radioMarginWidth,
23540
- width: circleSize - 2 * radioMarginWidth,
23541
- ":checked": {
23542
- backgroundColor: "#fff"
23543
- }
23544
- },
23545
- disableTextSelection: {
23546
- userSelect: "none"
23547
- }
23548
- });
23549
-
23550
23065
  /**
23551
23066
  * A small collection of useful scrolling utility functions.
23552
23067
  */
@@ -23923,8 +23438,8 @@ const styles$s = aphrodite.StyleSheet.create({
23923
23438
  crossOutLine: {
23924
23439
  // Center the icon within the container.
23925
23440
  position: "absolute",
23926
- top: `0px`,
23927
- left: `0px`
23441
+ top: `2px`,
23442
+ left: `2px`
23928
23443
  }
23929
23444
  });
23930
23445
 
@@ -23951,43 +23466,6 @@ function ChoiceInner(props) {
23951
23466
  }
23952
23467
  return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
23953
23468
  }
23954
-
23955
- // Handle dynamic styling of the multiple choice icon. Most
23956
- // MC icon styles are constant, but we do allow the caller
23957
- // to specify the selected color, and thus must control styles
23958
- // related to the selected state dynamically.
23959
- function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct) {
23960
- let backgroundColor;
23961
- let borderColor;
23962
- let color;
23963
- if (!showCorrectness && pressed) {
23964
- borderColor = wonderBlocksTokens.color.blue;
23965
- color = wonderBlocksTokens.color.blue;
23966
- backgroundColor = "transparent";
23967
- } else if (checked) {
23968
- const bg = showCorrectness && correct ? wonderBlocksTokens.color.green : wonderBlocksTokens.color.blue;
23969
- color = white;
23970
- backgroundColor = bg;
23971
- borderColor = bg;
23972
- } else {
23973
- borderColor = wonderBlocksTokens.color.offBlack64;
23974
- color = wonderBlocksTokens.color.offBlack64;
23975
- }
23976
-
23977
- // define shape
23978
- let borderRadius;
23979
- if (multipleSelect) {
23980
- borderRadius = 3;
23981
- } else {
23982
- borderRadius = CHOICE_ICON_SIZE;
23983
- }
23984
- return {
23985
- backgroundColor,
23986
- borderColor,
23987
- color,
23988
- borderRadius
23989
- };
23990
- }
23991
23469
  const ChoiceIcon = function (props) {
23992
23470
  const {
23993
23471
  checked,
@@ -23999,9 +23477,41 @@ const ChoiceIcon = function (props) {
23999
23477
  multipleSelect,
24000
23478
  pos,
24001
23479
  previouslyAnswered,
23480
+ // Used in "review mode"/"show rationale" to show that option was previously chosen
24002
23481
  pressed
24003
23482
  } = props;
24004
- const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
23483
+
23484
+ // Accounts for incorrect choice still displaying as learner tries again
23485
+ const choiceIsChecked = checked || showCorrectness && !correct && previouslyAnswered;
23486
+
23487
+ // Core styling
23488
+ const choiceStyling = [styles$r.choiceBase, multipleSelect ? styles$r.multiSelectShape : styles$r.singleSelectShape, showCorrectness ? styles$r.choiceHasIcon : styles$r.choiceHasLetter, choiceIsChecked ? styles$r.choiceIsChecked : styles$r.choiceIsUnchecked];
23489
+
23490
+ // Color styling
23491
+ // Handle dynamic styling of the multiple choice icon. Most
23492
+ // MC icon styles are constant, but we do allow the caller
23493
+ // to specify the selected color, and thus must control styles
23494
+ // related to the selected state dynamically.
23495
+ let crossOutColor;
23496
+ if (showCorrectness && correct && checked) {
23497
+ choiceStyling.push(styles$r.choiceCorrect);
23498
+ crossOutColor = wonderBlocksTokens.color.green;
23499
+ } else if (showCorrectness && !correct && (checked || previouslyAnswered)) {
23500
+ choiceStyling.push(styles$r.choiceIncorrect);
23501
+ crossOutColor = wonderBlocksTokens.color.red;
23502
+ } else if (checked) {
23503
+ // Show filled neutral blue color (showCorrectness is false)
23504
+ choiceStyling.push(styles$r.choiceNeutral);
23505
+ crossOutColor = wonderBlocksTokens.color.blue;
23506
+ } else if (pressed) {
23507
+ // Show outlined neutral blue color (showCorrectness is false)
23508
+ choiceStyling.push(styles$r.activeNeutral);
23509
+ crossOutColor = wonderBlocksTokens.color.blue;
23510
+ } else {
23511
+ // choice is not checked
23512
+ choiceStyling.push(styles$r.uncheckedColors);
23513
+ crossOutColor = wonderBlocksTokens.color.offBlack64;
23514
+ }
24005
23515
  return /*#__PURE__*/React__namespace.createElement("div", {
24006
23516
  className: aphrodite.css(styles$t.iconWrapper)
24007
23517
  }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
@@ -24009,9 +23519,8 @@ const ChoiceIcon = function (props) {
24009
23519
  visible: focused || hovered,
24010
23520
  multipleSelect: multipleSelect
24011
23521
  }, /*#__PURE__*/React__namespace.createElement("div", {
24012
- style: dynamicStyles,
24013
23522
  "data-testid": "choice-icon__library-choice-icon",
24014
- className: aphrodite.css(styles$r.circle, showCorrectness && correct && styles$r.circleCorrect, showCorrectness && !correct && styles$r.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$r.circleIncorrectAnswered)
23523
+ className: aphrodite.css(...choiceStyling)
24015
23524
  // used in BaseRadio editMode to check
24016
23525
  // if we actually clicked on the radio icon
24017
23526
  ,
@@ -24023,7 +23532,7 @@ const ChoiceIcon = function (props) {
24023
23532
  showCorrectness: showCorrectness,
24024
23533
  correct: correct
24025
23534
  })))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24026
- color: dynamicStyles.borderColor
23535
+ color: crossOutColor
24027
23536
  }));
24028
23537
  };
24029
23538
  const styles$r = aphrodite.StyleSheet.create({
@@ -24035,8 +23544,7 @@ const styles$r = aphrodite.StyleSheet.create({
24035
23544
  alignItems: "center",
24036
23545
  justifyContent: "center"
24037
23546
  },
24038
- circle: {
24039
- // Make the circle
23547
+ choiceBase: {
24040
23548
  width: CHOICE_ICON_SIZE,
24041
23549
  height: CHOICE_ICON_SIZE,
24042
23550
  boxSizing: "border-box",
@@ -24048,30 +23556,48 @@ const styles$r = aphrodite.StyleSheet.create({
24048
23556
  // "bold font family" so that characters which fall back to the default
24049
23557
  // font get bolded too.
24050
23558
  fontWeight: "bold",
24051
- fontSize: 12,
24052
23559
  // Center the icon wrapper.
24053
23560
  display: "flex",
24054
23561
  alignItems: "center",
24055
- justifyContent: "center",
24056
- // HACK(emily): I don't know why adding this line height makes the text
24057
- // appear centered better than any other value, but it does. In
24058
- // particular, at large zoom levels this line height does almost
24059
- // nothing, but at the default size this shifts the letter down one
24060
- // pixel so it is much better centered.
24061
- lineHeight: "1px"
24062
- },
24063
- circleCorrect: {
23562
+ justifyContent: "center"
23563
+ },
23564
+ choiceHasLetter: {
23565
+ fontSize: 12
23566
+ },
23567
+ choiceHasIcon: {
24064
23568
  fontSize: CHOICE_ICON_SIZE
24065
23569
  },
24066
- circleIncorrect: {
24067
- fontSize: CHOICE_ICON_SIZE,
24068
- borderColor: gray68,
24069
- color: gray68
23570
+ choiceIsChecked: {
23571
+ color: wonderBlocksTokens.color.white
23572
+ },
23573
+ choiceIsUnchecked: {
23574
+ color: wonderBlocksTokens.color.offBlack64
23575
+ },
23576
+ choiceCorrect: {
23577
+ backgroundColor: wonderBlocksTokens.color.green,
23578
+ borderColor: wonderBlocksTokens.color.green
24070
23579
  },
24071
- circleIncorrectAnswered: {
23580
+ choiceIncorrect: {
24072
23581
  backgroundColor: wonderBlocksTokens.color.red,
24073
- borderColor: wonderBlocksTokens.color.red,
24074
- color: wonderBlocksTokens.color.white
23582
+ borderColor: wonderBlocksTokens.color.red
23583
+ },
23584
+ choiceNeutral: {
23585
+ backgroundColor: wonderBlocksTokens.color.blue,
23586
+ borderColor: wonderBlocksTokens.color.blue
23587
+ },
23588
+ activeNeutral: {
23589
+ color: wonderBlocksTokens.color.blue,
23590
+ borderColor: wonderBlocksTokens.color.blue,
23591
+ backgroundColor: "transparent"
23592
+ },
23593
+ multiSelectShape: {
23594
+ borderRadius: 3
23595
+ },
23596
+ singleSelectShape: {
23597
+ borderRadius: CHOICE_ICON_SIZE
23598
+ },
23599
+ uncheckedColors: {
23600
+ borderColor: wonderBlocksTokens.color.offBlack64
24075
23601
  }
24076
23602
  });
24077
23603
 
@@ -24615,7 +24141,7 @@ const BaseRadio = function (_ref) {
24615
24141
  const aphroditeClassName = checked => {
24616
24142
  // Whether or not to show correctness borders
24617
24143
  // for this choice and the next choice.
24618
- return aphrodite.css(sharedStyles.aboveScratchpad, styles$o.item, styles$o.responsiveItem, checked && styles$o.selectedItem, checked && choice.highlighted && styles$o.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$o.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$o.nextHighlighted);
24144
+ return aphrodite.css(styles$o.item, styles$o.responsiveItem, checked && styles$o.selectedItem, checked && choice.highlighted && styles$o.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$o.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$o.nextHighlighted);
24619
24145
  };
24620
24146
 
24621
24147
  // HACK(abdulrahman): Preloads the selection-state
@@ -25220,17 +24746,105 @@ var Radio = {
25220
24746
  version: perseusCore.radioLogic.version,
25221
24747
  propUpgrades: perseusCore.radioLogic.widgetOptionsUpgrades,
25222
24748
  isLintable: true,
25223
- // TODO(LEMS-2656): remove TS suppression
25224
- // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
25225
- scorer: perseusScore.scoreRadio,
25226
- // TODO(LEMS-2656): remove TS suppression
25227
- // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
25228
- validator: perseusScore.validateRadio
24749
+ getPublicWidgetOptions: perseusCore.getRadioPublicWidgetOptions
25229
24750
  };
25230
24751
 
25231
24752
  /* eslint-disable import/no-named-as-default */
25232
24753
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
25233
24754
 
24755
+ const {
24756
+ radioBorderColor,
24757
+ checkedColor,
24758
+ circleSize,
24759
+ radioMarginWidth
24760
+ } = constants;
24761
+ var sharedStyles = aphrodite.StyleSheet.create({
24762
+ blankBackground: {
24763
+ // TODO(emily): Use WB colors?
24764
+ backgroundColor: "#FDFDFD"
24765
+ },
24766
+ perseusSrOnly: {
24767
+ border: 0,
24768
+ clip: "rect(0,0,0,0)",
24769
+ height: 1,
24770
+ margin: -1,
24771
+ overflow: "hidden",
24772
+ padding: 0,
24773
+ position: "absolute",
24774
+ width: 1
24775
+ },
24776
+ responsiveLabel: {
24777
+ // NOTE(charlie): The values used here should be kept in sync with the
24778
+ // caption text sizing in Perseus' styles.less.
24779
+ // TODO(charlie): Migrate the captions over to using this style.
24780
+ [mediaQueries.smOrSmaller]: {
24781
+ fontSize: 14,
24782
+ lineHeight: 1.3
24783
+ },
24784
+ [mediaQueries.md]: {
24785
+ fontSize: 17,
24786
+ lineHeight: 1.4
24787
+ },
24788
+ [mediaQueries.lgOrLarger]: {
24789
+ fontSize: 20,
24790
+ lineHeight: 1.4
24791
+ }
24792
+ },
24793
+ responsiveInput: {
24794
+ display: "inline-block",
24795
+ WebkitAppearance: "none",
24796
+ appearance: "none",
24797
+ "::-ms-check": {
24798
+ display: "none"
24799
+ },
24800
+ backgroundColor: "#fff",
24801
+ border: "2px solid #fff",
24802
+ boxShadow: `0 0px 0px 1px ${radioBorderColor}`,
24803
+ outline: "none",
24804
+ boxSizing: "border-box",
24805
+ flexShrink: 0,
24806
+ marginBottom: 1,
24807
+ marginLeft: 1,
24808
+ marginRight: 1,
24809
+ marginTop: 1,
24810
+ height: circleSize - 2,
24811
+ width: circleSize - 2
24812
+ },
24813
+ responsiveRadioInput: {
24814
+ borderRadius: "50%",
24815
+ ":checked": {
24816
+ backgroundColor: checkedColor,
24817
+ border: "none",
24818
+ borderRadius: "50%",
24819
+ boxShadow: `inset 0px 0px 0px 2px white, ` + `0 0px 0px 2px ${checkedColor}`,
24820
+ marginTop: radioMarginWidth,
24821
+ marginBottom: radioMarginWidth,
24822
+ marginLeft: radioMarginWidth,
24823
+ marginRight: radioMarginWidth,
24824
+ height: circleSize - 2 * radioMarginWidth,
24825
+ width: circleSize - 2 * radioMarginWidth
24826
+ }
24827
+ },
24828
+ responsiveRadioInputActive: {
24829
+ backgroundColor: "#fff",
24830
+ border: "2px solid #fff",
24831
+ borderRadius: "50%",
24832
+ boxShadow: `0 0px 0px 2px ${checkedColor}`,
24833
+ marginTop: radioMarginWidth,
24834
+ marginBottom: radioMarginWidth,
24835
+ marginLeft: radioMarginWidth,
24836
+ marginRight: radioMarginWidth,
24837
+ height: circleSize - 2 * radioMarginWidth,
24838
+ width: circleSize - 2 * radioMarginWidth,
24839
+ ":checked": {
24840
+ backgroundColor: "#fff"
24841
+ }
24842
+ },
24843
+ disableTextSelection: {
24844
+ userSelect: "none"
24845
+ }
24846
+ });
24847
+
25234
24848
  const getPromptJSON$n = (renderProps, userInput) => {
25235
24849
  return {
25236
24850
  type: "categorizer",
@@ -25331,7 +24945,6 @@ class Categorizer extends React__namespace.Component {
25331
24945
  className: "category " + aphrodite.css(styles$n.cell),
25332
24946
  key: catNum
25333
24947
  }, /*#__PURE__*/React__namespace.createElement("div", {
25334
- className: ClassNames.INTERACTIVE,
25335
24948
  role: "button",
25336
24949
  "aria-label": catName,
25337
24950
  onClick: () => this.onChange(itemNum, catNum)
@@ -25423,12 +25036,6 @@ var Categorizer$1 = {
25423
25036
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
25424
25037
  },
25425
25038
  isLintable: true,
25426
- // TODO(LEMS-2656): remove TS suppression
25427
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25428
- scorer: perseusScore.scoreCategorizer,
25429
- // TODO(LEMS-2656): remove TS suppression
25430
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25431
- validator: perseusScore.validateCategorizer,
25432
25039
  getPublicWidgetOptions: perseusCore.getCategorizerPublicWidgetOptions
25433
25040
  };
25434
25041
 
@@ -25638,12 +25245,9 @@ const styles$m = aphrodite.StyleSheet.create({
25638
25245
  var CSProgram$1 = {
25639
25246
  name: "cs-program",
25640
25247
  displayName: "CS Program",
25641
- supportedAlignments: ["block", "full-width"],
25642
25248
  widget: CSProgram,
25643
25249
  hidden: true,
25644
- // TODO(LEMS-2656): remove TS suppression
25645
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25646
- scorer: perseusScore.scoreCSProgram
25250
+ getPublicWidgetOptions: perseusCore.getCSProgramPublicWidgetOptions
25647
25251
  };
25648
25252
 
25649
25253
  const getPromptJSON$l = renderProps => {
@@ -25720,11 +25324,8 @@ var Definition$1 = {
25720
25324
  name: "definition",
25721
25325
  displayName: "Definition",
25722
25326
  accessible: true,
25723
- defaultAlignment: "inline",
25724
25327
  widget: Definition,
25725
- transform: x => x,
25726
- // TODO: things that aren't interactive shouldn't need scoring functions
25727
- scorer: () => scoreNoop()
25328
+ transform: x => x
25728
25329
  };
25729
25330
 
25730
25331
  // There are no required props, but this component can receive props from any
@@ -25751,9 +25352,7 @@ var DeprecatedStandin$1 = {
25751
25352
  name: "deprecated-standin",
25752
25353
  displayName: "Deprecated Standin",
25753
25354
  widget: DeprecatedStandin,
25754
- hidden: true,
25755
- // TODO: things that aren't interactive shouldn't need scoring functions
25756
- scorer: () => scoreNoop(1)
25355
+ hidden: true
25757
25356
  };
25758
25357
 
25759
25358
  const getPromptJSON$k = (renderProps, userInput) => {
@@ -25862,16 +25461,10 @@ const optionsTransform = widgetOptions => {
25862
25461
  var Dropdown$1 = {
25863
25462
  name: "dropdown",
25864
25463
  displayName: "Drop down",
25865
- defaultAlignment: "inline-block",
25866
25464
  accessible: true,
25867
25465
  widget: Dropdown,
25868
25466
  transform: optionsTransform,
25869
- // TODO(LEMS-2656): remove TS suppression
25870
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25871
- scorer: perseusScore.scoreDropdown,
25872
- // TODO(LEMS-2656): remove TS suppression
25873
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25874
- validator: perseusScore.validateDropdown
25467
+ getPublicWidgetOptions: perseusCore.getDropdownPublicWidgetOptions
25875
25468
  };
25876
25469
 
25877
25470
  const getPromptJSON$j = renderProps => {
@@ -26030,12 +25623,9 @@ var Explanation$1 = {
26030
25623
  name: "explanation",
26031
25624
  displayName: "Explanation",
26032
25625
  accessible: true,
26033
- defaultAlignment: "inline",
26034
25626
  widget: Explanation,
26035
25627
  transform: ___default["default"].identity,
26036
- isLintable: true,
26037
- // TODO: things that aren't interactive shouldn't need scoring functions
26038
- scorer: () => scoreNoop()
25628
+ isLintable: true
26039
25629
  };
26040
25630
 
26041
25631
  const getPromptJSON$i = (title, rendererJSON, hintRendererJSON) => {
@@ -27156,7 +26746,7 @@ class FunctionGrapher extends React__namespace.Component {
27156
26746
  boxSizing: "initial"
27157
26747
  }
27158
26748
  }, /*#__PURE__*/React__namespace.createElement("div", {
27159
- className: "graphie-container above-scratchpad",
26749
+ className: "graphie-container blank-background",
27160
26750
  style: {
27161
26751
  width: box[0],
27162
26752
  height: box[1]
@@ -27284,8 +26874,7 @@ class Grapher extends React__namespace.Component {
27284
26874
  const coords = this.props.plot.coords;
27285
26875
  const asymptote = this.props.plot.asymptote;
27286
26876
  const typeSelector = /*#__PURE__*/React__namespace.createElement("div", {
27287
- style: typeSelectorStyle,
27288
- className: "above-scratchpad"
26877
+ style: typeSelectorStyle
27289
26878
  }, /*#__PURE__*/React__namespace.createElement(ButtonGroup, {
27290
26879
  value: type,
27291
26880
  allowEmpty: true,
@@ -27368,9 +26957,7 @@ var Grapher$1 = {
27368
26957
  widget: Grapher,
27369
26958
  transform: propTransform$2,
27370
26959
  staticTransform: staticTransform$4,
27371
- // TODO(LEMS-2656): remove TS suppression
27372
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
27373
- scorer: perseusScore.scoreGrapher
26960
+ getPublicWidgetOptions: perseusCore.getGrapherPublicWidgetOptions
27374
26961
  };
27375
26962
 
27376
26963
  const getPromptJSON$f = rendererJSON => {
@@ -27387,24 +26974,6 @@ const getPromptJSON$f = rendererJSON => {
27387
26974
  };
27388
26975
  };
27389
26976
 
27390
- // The `group` widget is basically a widget hosting a full Perseus system in
27391
- // it. As such, scoring a group means scoring all widgets it contains.
27392
- function scoreGroup(userInput, rubric, strings, locale) {
27393
- const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, strings, locale);
27394
- return flattenScores(scores);
27395
- }
27396
-
27397
- function validateGroup(userInput, validationData, strings, locale) {
27398
- const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, strings, locale);
27399
- if (emptyWidgets.length === 0) {
27400
- return null;
27401
- }
27402
- return {
27403
- type: "invalid",
27404
- message: null
27405
- };
27406
- }
27407
-
27408
26977
  // exports has no 'transform'
27409
26978
 
27410
26979
  class Group extends React__namespace.Component {
@@ -27556,12 +27125,6 @@ var Group$1 = {
27556
27125
  displayName: "Group (SAT only)",
27557
27126
  widget: Group,
27558
27127
  traverseChildWidgets: traverseChildWidgets,
27559
- // TODO(LEMS-2656): remove TS suppression
27560
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27561
- scorer: scoreGroup,
27562
- // TODO(LEMS-2656): remove TS suppression
27563
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27564
- validator: validateGroup,
27565
27128
  hidden: true,
27566
27129
  isLintable: true
27567
27130
  };
@@ -27702,9 +27265,7 @@ var Iframe$1 = {
27702
27265
  widget: Iframe,
27703
27266
  // Let's not expose it to all content creators yet
27704
27267
  hidden: true,
27705
- // TODO(LEMS-2656): remove TS suppression
27706
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
27707
- scorer: perseusScore.scoreIframe
27268
+ getPublicWidgetOptions: perseusCore.getIFramePublicWidgetOptions
27708
27269
  };
27709
27270
 
27710
27271
  const getPromptJSON$d = renderProps => {
@@ -27726,15 +27287,13 @@ const defaultBackgroundImage$2 = {
27726
27287
  width: 0,
27727
27288
  height: 0
27728
27289
  };
27729
- const editorAlignments = ["block", "full-width"];
27730
- const DEFAULT_ALIGNMENT = "block";
27731
27290
 
27732
27291
  // there is no transform as part of exports
27733
27292
 
27734
27293
  class ImageWidget extends React__namespace.Component {
27735
27294
  static contextType = PerseusI18nContext;
27736
27295
  static defaultProps = {
27737
- alignment: DEFAULT_ALIGNMENT,
27296
+ alignment: "block",
27738
27297
  title: "",
27739
27298
  range: [defaultRange, defaultRange],
27740
27299
  box: [defaultBoxSize, defaultBoxSize],
@@ -27901,13 +27460,9 @@ var Image$1 = {
27901
27460
  const bgImage = widgetOptions.backgroundImage;
27902
27461
  return !(bgImage && bgImage.url && !widgetOptions.alt);
27903
27462
  },
27904
- defaultAlignment: DEFAULT_ALIGNMENT,
27905
- supportedAlignments: editorAlignments,
27906
27463
  displayName: "Image",
27907
27464
  widget: ImageWidget,
27908
- isLintable: true,
27909
- // TODO: things that aren't interactive shouldn't need scoring functions
27910
- scorer: () => scoreNoop()
27465
+ isLintable: true
27911
27466
  };
27912
27467
 
27913
27468
  const getPromptJSON$c = () => {
@@ -28510,9 +28065,7 @@ var Interactive = {
28510
28065
  displayName: "Interaction",
28511
28066
  widget: Interaction,
28512
28067
  transform: ___default["default"].identity,
28513
- hidden: true,
28514
- // TODO: things that aren't interactive shouldn't need scoring functions
28515
- scorer: () => scoreNoop()
28068
+ hidden: true
28516
28069
  };
28517
28070
 
28518
28071
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -28794,7 +28347,7 @@ class Graph extends React__namespace.Component {
28794
28347
  image = null;
28795
28348
  }
28796
28349
  return /*#__PURE__*/React__namespace.createElement("div", {
28797
- className: "graphie-container above-scratchpad",
28350
+ className: "graphie-container blank-background",
28798
28351
  style: {
28799
28352
  width: this.props.box[0],
28800
28353
  height: this.props.box[1]
@@ -29428,7 +28981,8 @@ const YGridTick = _ref => {
29428
28981
  let {
29429
28982
  y,
29430
28983
  range,
29431
- tickStep
28984
+ tickStep,
28985
+ showPi
29432
28986
  } = _ref;
29433
28987
  // If the graph requires out-of-bounds labels, we want to make sure to set the
29434
28988
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -29464,6 +29018,7 @@ const YGridTick = _ref => {
29464
29018
  // If the graph displays both the y and x axis lines within the graph, we want
29465
29019
  // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
29466
29020
  const showLabel = shouldShowLabel(y, range, tickStep);
29021
+ const yLabel = showPi ? divideByAndShowPi(y) : y.toString();
29467
29022
  return /*#__PURE__*/React__namespace.createElement("g", {
29468
29023
  className: "tick",
29469
29024
  "aria-hidden": true
@@ -29481,12 +29036,13 @@ const YGridTick = _ref => {
29481
29036
  textAnchor: "end",
29482
29037
  x: xPositionText,
29483
29038
  y: yPositionText
29484
- }, y.toString()));
29039
+ }, yLabel));
29485
29040
  };
29486
29041
  const XGridTick = _ref2 => {
29487
29042
  let {
29488
29043
  x,
29489
- range
29044
+ range,
29045
+ showPi
29490
29046
  } = _ref2;
29491
29047
  // If the graph requires out-of-bounds labels, we want to make sure to set the
29492
29048
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -29525,6 +29081,7 @@ const XGridTick = _ref2 => {
29525
29081
  // Apply the adjustments to the x and y positions for the text
29526
29082
  const xPositionText = xPosition + xAdjustment;
29527
29083
  const yPositionText = yPosition + yAdjustment;
29084
+ const xLabel = showPi ? divideByAndShowPi(x) : x.toString();
29528
29085
  return /*#__PURE__*/React__namespace.createElement("g", {
29529
29086
  className: "tick",
29530
29087
  "aria-hidden": true
@@ -29542,7 +29099,7 @@ const XGridTick = _ref2 => {
29542
29099
  textAnchor: "middle",
29543
29100
  x: xPositionText,
29544
29101
  y: yPositionText
29545
- }, x.toString()));
29102
+ }, xLabel));
29546
29103
  };
29547
29104
 
29548
29105
  // Determines whether to show the label for the given tick
@@ -29589,6 +29146,22 @@ const countSignificantDecimals = number => {
29589
29146
  }
29590
29147
  return numStr.split(".")[1].length;
29591
29148
  };
29149
+
29150
+ // Show the given value as a multiple of pi (already assumed to be
29151
+ // a multiple of pi). Exported for testing
29152
+ function divideByAndShowPi(value) {
29153
+ const dividedValue = value / Math.PI;
29154
+ switch (dividedValue) {
29155
+ case 1:
29156
+ return "π";
29157
+ case -1:
29158
+ return "-π";
29159
+ case 0:
29160
+ return "0";
29161
+ default:
29162
+ return dividedValue + "π";
29163
+ }
29164
+ }
29592
29165
  const AxisTicks = () => {
29593
29166
  const {
29594
29167
  tickStep,
@@ -29611,6 +29184,10 @@ const AxisTicks = () => {
29611
29184
  key: `y-grid-tick-${y}`,
29612
29185
  range: range,
29613
29186
  tickStep: tickStep[Y]
29187
+ // Show the tick labels as multiples of pi
29188
+ // if the tick step is a multiple of pi.
29189
+ ,
29190
+ showPi: tickStep[Y] % Math.PI === 0
29614
29191
  });
29615
29192
  })), /*#__PURE__*/React__namespace.createElement("g", {
29616
29193
  className: "x-axis-ticks"
@@ -29619,6 +29196,10 @@ const AxisTicks = () => {
29619
29196
  x: x,
29620
29197
  key: `x-grid-tick-${x}`,
29621
29198
  range: range
29199
+ // Show the tick labels as multiples of pi
29200
+ // if the tick step is a multiple of pi.
29201
+ ,
29202
+ showPi: tickStep[X] % Math.PI === 0
29622
29203
  });
29623
29204
  })));
29624
29205
  };
@@ -29920,6 +29501,15 @@ const styles$d = aphrodite.StyleSheet.create({
29920
29501
  }
29921
29502
  });
29922
29503
 
29504
+ function isPiMultiple(value) {
29505
+ // Early return for integers that clearly aren't multiples
29506
+ // of pi to save some computation.
29507
+ if (Number.isInteger(value)) {
29508
+ return false;
29509
+ }
29510
+ return value % Math.PI === 0 || value % (Math.PI / 2) === 0 || value % (Math.PI / 3) === 0 || value % (Math.PI / 4) === 0 || value % (Math.PI / 6) === 0;
29511
+ }
29512
+
29923
29513
  const {
29924
29514
  firstNumericalParse,
29925
29515
  captureScratchpadTouchStart
@@ -29957,7 +29547,8 @@ class NumberInput extends React__namespace.Component {
29957
29547
  onFormatChange: PropTypes__default["default"].func,
29958
29548
  checkValidity: PropTypes__default["default"].func,
29959
29549
  size: PropTypes__default["default"].oneOf(["mini", "small", "normal"]),
29960
- label: PropTypes__default["default"].oneOf(["put your labels outside your inputs!"])
29550
+ label: PropTypes__default["default"].oneOf(["put your labels outside your inputs!"]),
29551
+ allowPiTruncation: PropTypes__default["default"].bool
29961
29552
  };
29962
29553
  static defaultProps = {
29963
29554
  value: null,
@@ -29970,6 +29561,19 @@ class NumberInput extends React__namespace.Component {
29970
29561
  state = {
29971
29562
  format: this.props.format
29972
29563
  };
29564
+ componentDidMount() {
29565
+ // If the value is a multiple of pi, but it is not in the pi format,
29566
+ // then convert it to the pi format and show it as a multiple of pi.
29567
+ const value = this.getValue();
29568
+ if (this.props.allowPiTruncation && value !== null && value !== 0) {
29569
+ if (this.state.format !== "pi" && isPiMultiple(value)) {
29570
+ this._setValue(value / Math.PI, "pi");
29571
+ this.setState({
29572
+ format: "pi"
29573
+ });
29574
+ }
29575
+ }
29576
+ }
29973
29577
  componentDidUpdate(prevProps) {
29974
29578
  if (!kmath.number.equal(this.getValue(), this.props.value)) {
29975
29579
  this._setValue(this.props.value, this.state.format);
@@ -30126,7 +29730,8 @@ class RangeInput extends React__namespace.Component {
30126
29730
  value: PropTypes__default["default"].array.isRequired,
30127
29731
  onChange: PropTypes__default["default"].func.isRequired,
30128
29732
  placeholder: PropTypes__default["default"].array,
30129
- checkValidity: PropTypes__default["default"].func
29733
+ checkValidity: PropTypes__default["default"].func,
29734
+ allowPiTruncation: PropTypes__default["default"].bool
30130
29735
  };
30131
29736
  static defaultProps = {
30132
29737
  placeholder: [null, null]
@@ -30150,14 +29755,16 @@ class RangeInput extends React__namespace.Component {
30150
29755
  // eslint-disable-next-line react/jsx-no-bind
30151
29756
  ,
30152
29757
  onChange: this.onChange.bind(this, 0),
30153
- placeholder: this.props.placeholder[0]
29758
+ placeholder: this.props.placeholder[0],
29759
+ allowPiTruncation: this.props.allowPiTruncation
30154
29760
  })), /*#__PURE__*/React__namespace.createElement(NumberInput, _extends({}, this.props, {
30155
29761
  value: value[1],
30156
29762
  checkValidity: val => checkValidity([value[0], val])
30157
29763
  // eslint-disable-next-line react/jsx-no-bind
30158
29764
  ,
30159
29765
  onChange: this.onChange.bind(this, 1),
30160
- placeholder: this.props.placeholder[1]
29766
+ placeholder: this.props.placeholder[1],
29767
+ allowPiTruncation: this.props.allowPiTruncation
30161
29768
  })));
30162
29769
  }
30163
29770
  }
@@ -30390,10 +29997,10 @@ function Vector(props) {
30390
29997
  }));
30391
29998
  }
30392
29999
 
30393
- function srFormatNumber(a, locale) {
30000
+ function srFormatNumber(a, locale, maximumFractionDigits) {
30394
30001
  // adding zero here converts negative zero to positive zero.
30395
30002
  return (0 + a).toLocaleString(locale, {
30396
- maximumFractionDigits: 3,
30003
+ maximumFractionDigits: maximumFractionDigits ?? 3,
30397
30004
  useGrouping: false // no thousands separators
30398
30005
  });
30399
30006
  }
@@ -30572,6 +30179,95 @@ a, b, c) {
30572
30179
  return [x1, x2];
30573
30180
  }
30574
30181
 
30182
+ /**
30183
+ * Given a list of points, return the angle made by the point at index i
30184
+ * with the points at indices i - 1 and i + 1.
30185
+ */
30186
+ function getAngleFromPoints(points, i) {
30187
+ // Make sure the index is valid.
30188
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
30189
+ return null;
30190
+ }
30191
+
30192
+ // If there are fewer than 3 points, there is no angle.
30193
+ if (points.length < 3) {
30194
+ return null;
30195
+ }
30196
+
30197
+ // Get the points at the current index and the two adjacent points.
30198
+ const point = points.at(i);
30199
+ const pt1 = points.at(i - 1);
30200
+ const pt2 = points[(i + 1) % points.length];
30201
+ if (!point || !pt1 || !pt2) {
30202
+ return null;
30203
+ }
30204
+
30205
+ // Get the lengths of adjacent sides, making a triangle
30206
+ // we can use to calculate the angle.
30207
+ const a = mafs.vec.dist(point, pt1);
30208
+ const b = mafs.vec.dist(point, pt2);
30209
+ const c = mafs.vec.dist(pt1, pt2);
30210
+
30211
+ // Law of cosines
30212
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
30213
+ return angle;
30214
+ }
30215
+ function getSideLengthsFromPoints(points, i, isPolygonOpen) {
30216
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
30217
+ return [];
30218
+ }
30219
+ if (points.length < 2) {
30220
+ return [];
30221
+ }
30222
+ const returnArray = [];
30223
+ const point = points[i];
30224
+ // If this point is the first point, then side 1 is the
30225
+ // last point in the list.
30226
+ const point1Index = i === 0 ? points.length - 1 : i - 1;
30227
+ const point1 = points[point1Index];
30228
+ // Make sure the previous point is not the same
30229
+ // as the current point.
30230
+ const side1 = i !== point1Index ? mafs.vec.dist(point, point1) : null;
30231
+ // The first side of the first point does not exist if
30232
+ // the polygon is open.
30233
+ if (side1 && !(isPolygonOpen && i === 0)) {
30234
+ returnArray.push({
30235
+ pointIndex: point1Index,
30236
+ sideLength: side1
30237
+ });
30238
+ }
30239
+
30240
+ // See if there is a side 2.
30241
+ const point2Index = (i + 1) % points.length;
30242
+ const point2 = points[point2Index];
30243
+ // Make sure that the next point is not the same as the
30244
+ // current point, and don't repeat the first point.
30245
+ const side2 = i !== point2Index && point2Index !== point1Index ? mafs.vec.dist(point, point2) : null;
30246
+ // The second side of the last point does not exist if
30247
+ // the polygon is open.
30248
+ if (side2 && !(isPolygonOpen && i === points.length - 1)) {
30249
+ returnArray.push({
30250
+ pointIndex: point2Index,
30251
+ sideLength: side2
30252
+ });
30253
+ }
30254
+ return returnArray;
30255
+ }
30256
+
30257
+ /**
30258
+ * Determine whether to determine the string with the
30259
+ * exact side length or the approximate side length.
30260
+ */
30261
+ function getPolygonSideString(sideLength, pointIndex, strings, locale) {
30262
+ return Number.isInteger(sideLength) ? strings.srPolygonSideLength({
30263
+ pointNum: pointIndex + 1,
30264
+ length: `${sideLength}`
30265
+ }) : strings.srPolygonSideLengthApprox({
30266
+ pointNum: pointIndex + 1,
30267
+ length: srFormatNumber(sideLength, locale, 1)
30268
+ });
30269
+ }
30270
+
30575
30271
  const {
30576
30272
  calculateAngleInDegrees: calculateAngleInDegrees$2
30577
30273
  } = kmath.angles;
@@ -31019,7 +30715,7 @@ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs
31019
30715
  })));
31020
30716
 
31021
30717
  const {
31022
- clockwise: clockwise$1
30718
+ clockwise
31023
30719
  } = kmath.geometry;
31024
30720
  const {
31025
30721
  getAngleFromVertex: getAngleFromVertex$1
@@ -31034,7 +30730,7 @@ const PolygonAngle = _ref => {
31034
30730
  snapTo
31035
30731
  } = _ref;
31036
30732
  const [centerX, centerY] = centerPoint;
31037
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
30733
+ const areClockwise = clockwise([centerPoint, ...endPoints]);
31038
30734
  const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
31039
30735
  const radius = 0.3;
31040
30736
  const a = mafs.vec.dist(centerPoint, endPoints[0]);
@@ -31107,7 +30803,7 @@ const Angle = _ref2 => {
31107
30803
  range
31108
30804
  } = _ref2;
31109
30805
  // Check if the points are clockwise or not, depending on whether we allow reflex angles
31110
- const areClockwise = clockwise$1([...coords, vertex]);
30806
+ const areClockwise = clockwise([...coords, vertex]);
31111
30807
  const shouldReverseCoords = areClockwise && !allowReflexAngles;
31112
30808
 
31113
30809
  // Reverse the coordinates accordingly to ensure the angle is calculated correctly
@@ -32054,9 +31750,6 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
32054
31750
  };
32055
31751
  }
32056
31752
 
32057
- const {
32058
- clockwise
32059
- } = kmath.geometry;
32060
31753
  function getGradableGraph(state, initialGraph) {
32061
31754
  if (!state.hasBeenInteractedWith) {
32062
31755
  return {
@@ -32133,17 +31826,9 @@ function getGradableGraph(state, initialGraph) {
32133
31826
  };
32134
31827
  }
32135
31828
  if (state.type === "angle" && initialGraph.type === "angle") {
32136
- // We're going to reverse the coords for scoring if the angle is clockwise and
32137
- // we don't allow reflex angles. This is because the angle is largely defined
32138
- // by the order of the points in the coords array, and we want to maintain
32139
- // the same angle scoring with the legacy graph (which had a bug).
32140
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
32141
- const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
32142
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
32143
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
32144
31829
  return {
32145
31830
  ...initialGraph,
32146
- coords,
31831
+ coords: state.coords,
32147
31832
  allowReflexAngles: state.allowReflexAngles
32148
31833
  };
32149
31834
  }
@@ -32795,17 +32480,20 @@ function describePointGraph(state, i18n) {
32795
32480
  });
32796
32481
  });
32797
32482
  return strings.srInteractiveElements({
32798
- elements: pointDescriptions.join(", ")
32483
+ elements: pointDescriptions.join(" ")
32799
32484
  });
32800
32485
  }
32801
32486
 
32802
- function renderPolygonGraph(state, dispatch) {
32487
+ const {
32488
+ convertRadiansToDegrees
32489
+ } = kmath.angles;
32490
+ function renderPolygonGraph(state, dispatch, i18n, markings) {
32803
32491
  return {
32804
32492
  graph: /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
32805
32493
  graphState: state,
32806
32494
  dispatch: dispatch
32807
32495
  }),
32808
- interactiveElementsDescription: null
32496
+ interactiveElementsDescription: getPolygonGraphDescription(state, i18n, markings)
32809
32497
  };
32810
32498
  }
32811
32499
  const PolygonGraph = props => {
@@ -32890,7 +32578,7 @@ const PolygonGraph = props => {
32890
32578
  focusVisible,
32891
32579
  setFocusVisible
32892
32580
  };
32893
- return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
32581
+ return numSides === "unlimited" ? /*#__PURE__*/React__namespace.createElement(UnlimitedPolygonGraph, statefulProps) : /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, statefulProps);
32894
32582
  };
32895
32583
  const LimitedPolygonGraph = statefulProps => {
32896
32584
  const {
@@ -32915,8 +32603,39 @@ const LimitedPolygonGraph = statefulProps => {
32915
32603
  const {
32916
32604
  disableKeyboardInteraction
32917
32605
  } = graphConfig;
32606
+ const {
32607
+ strings,
32608
+ locale
32609
+ } = usePerseusI18n();
32610
+ const id = React__namespace.useId();
32611
+ const pointsOffArray = Array(points.length).fill("off");
32612
+ // When moving an element, set its aria-live to "polite" and the others
32613
+ // to "off". Otherwise, other connected elements that move at the same
32614
+ // time might override the currently focused element's aria live.
32615
+ const [ariaLives, setAriaLives] = React__namespace.useState([
32616
+ // First one represents the aria-live value for the polygon itself.
32617
+ "off",
32618
+ // The rest represent the points.
32619
+ ...pointsOffArray]);
32918
32620
  const lines = getLines(points);
32919
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32621
+ const polygonPointsNumId = id + "-points-num";
32622
+ const polygonPointsId = id + "-points";
32623
+
32624
+ // Aria label srings
32625
+ const {
32626
+ srPolygonGraph,
32627
+ srPolygonGraphPointsNum,
32628
+ srPolygonGraphPoints,
32629
+ srPolygonElementsNum
32630
+ } = describePolygonGraph(statefulProps.graphState, {
32631
+ strings,
32632
+ locale
32633
+ }, statefulProps.graphConfig.markings);
32634
+ return /*#__PURE__*/React__namespace.createElement("g", {
32635
+ // Outer graph minimal description
32636
+ "aria-label": srPolygonGraph,
32637
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32638
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32920
32639
  points: [...points],
32921
32640
  color: "var(--movable-line-stroke-color)",
32922
32641
  svgPolygonProps: {
@@ -32942,13 +32661,17 @@ const LimitedPolygonGraph = statefulProps => {
32942
32661
  });
32943
32662
  }), showSides && lines.map((_ref, i) => {
32944
32663
  let [start, end] = _ref;
32664
+ // Use x and y to find the position of the label
32945
32665
  const [x, y] = mafs.vec.midpoint(start, end);
32946
- const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
32666
+ const length = mafs.vec.dist(start, end);
32667
+ // Check if the length needs to indicate
32668
+ // that it's an approximation.
32669
+ const isApprox = !Number.isInteger(length);
32947
32670
  return /*#__PURE__*/React__namespace.createElement(TextLabel, {
32948
32671
  key: "side-" + i,
32949
32672
  x: x,
32950
32673
  y: y
32951
- }, !Number.isInteger(length) && "≈ ", length);
32674
+ }, isApprox ? `≈ ${length.toFixed(snapTo === "sides" ? 0 : 1)}` : length);
32952
32675
  }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32953
32676
  points: [...points],
32954
32677
  color: "transparent",
@@ -32968,27 +32691,80 @@ const LimitedPolygonGraph = statefulProps => {
32968
32691
  setFocusVisible(hasFocusVisible(polygonRef.current));
32969
32692
  },
32970
32693
  // Required for lines to darken on focus
32971
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32694
+ onFocus: () => {
32695
+ setFocusVisible(hasFocusVisible(polygonRef.current));
32696
+ setAriaLives(() => ["polite", ...pointsOffArray]);
32697
+ },
32972
32698
  // Required for line weighting to update on blur. Without this,
32973
32699
  // the user has to hover over the shape for it to update
32974
32700
  onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32975
- className: "movable-polygon"
32976
- }
32977
- }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32978
- key: "point-" + i,
32979
- constrain: constrain,
32980
- point: point,
32981
- sequenceNumber: i + 1,
32982
- onMove: destination => {
32983
- const now = Date.now();
32984
- const targetFPS = 40;
32985
- const moveThresholdTime = 1000 / targetFPS;
32986
- if (now - lastMoveTimeRef.current > moveThresholdTime) {
32987
- dispatch(actions.polygon.movePoint(i, destination));
32988
- lastMoveTimeRef.current = now;
32989
- }
32701
+ className: "movable-polygon",
32702
+ // Accessibility-related fields
32703
+ role: "button",
32704
+ "aria-label": srPolygonGraphPoints ? `${srPolygonElementsNum} ${srPolygonGraphPoints}` : srPolygonElementsNum,
32705
+ "aria-live": ariaLives[0]
32990
32706
  }
32991
- })));
32707
+ }), points.map((point, i) => {
32708
+ const angleId = `${id}-angle-${i}`;
32709
+ const side1Id = `${id}-point-${i}-side-1`;
32710
+ const side2Id = `${id}-point-${i}-side-2`;
32711
+
32712
+ // Limited polygons always have an angle at each vertex.
32713
+ const angle = getAngleFromPoints(points, i);
32714
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32715
+
32716
+ // Limited polygons always two sides attached to each vertex.
32717
+ const sidesArray = getSideLengthsFromPoints(points, i);
32718
+ const {
32719
+ pointIndex: point1Index,
32720
+ sideLength: side1Length
32721
+ } = sidesArray[0];
32722
+ const {
32723
+ pointIndex: point2Index,
32724
+ sideLength: side2Length
32725
+ } = sidesArray[1];
32726
+ return /*#__PURE__*/React__namespace.createElement("g", {
32727
+ key: "point-" + i
32728
+ }, /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32729
+ ariaDescribedBy: `${angleId} ${side1Id} ${side2Id}`,
32730
+ ariaLive: ariaLives[i + 1],
32731
+ constrain: constrain,
32732
+ point: point,
32733
+ sequenceNumber: i + 1,
32734
+ onMove: destination => {
32735
+ const now = Date.now();
32736
+ const targetFPS = 40;
32737
+ const moveThresholdTime = 1000 / targetFPS;
32738
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
32739
+ dispatch(actions.polygon.movePoint(i, destination));
32740
+ lastMoveTimeRef.current = now;
32741
+ }
32742
+ },
32743
+ onFocus: () => {
32744
+ const newPointAriaLives = [...pointsOffArray];
32745
+ newPointAriaLives[i] = "polite";
32746
+ // Whole polygon is "off", and the current
32747
+ // point is "polite".
32748
+ setAriaLives(["off", ...newPointAriaLives]);
32749
+ }
32750
+ }), angleDegree && /*#__PURE__*/React__namespace.createElement("g", {
32751
+ id: angleId
32752
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32753
+ angle: angleDegree
32754
+ }) : strings.srPolygonPointAngleApprox({
32755
+ angle: srFormatNumber(angleDegree, locale, 1)
32756
+ })), /*#__PURE__*/React__namespace.createElement("g", {
32757
+ id: side1Id
32758
+ }, getPolygonSideString(side1Length, point1Index, strings, locale)), /*#__PURE__*/React__namespace.createElement("g", {
32759
+ id: side2Id
32760
+ }, getPolygonSideString(side2Length, point2Index, strings, locale)));
32761
+ }), /*#__PURE__*/React__namespace.createElement("g", {
32762
+ id: polygonPointsNumId,
32763
+ style: a11y.srOnly
32764
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React__namespace.createElement("g", {
32765
+ id: polygonPointsId,
32766
+ style: a11y.srOnly
32767
+ }, srPolygonGraphPoints));
32992
32768
  };
32993
32769
  const UnlimitedPolygonGraph = statefulProps => {
32994
32770
  const {
@@ -33003,6 +32779,19 @@ const UnlimitedPolygonGraph = statefulProps => {
33003
32779
  coords,
33004
32780
  closedPolygon
33005
32781
  } = statefulProps.graphState;
32782
+ const {
32783
+ strings,
32784
+ locale
32785
+ } = usePerseusI18n();
32786
+ const id = React__namespace.useId();
32787
+ const polygonPointsNumId = id + "-points-num";
32788
+ const polygonPointsId = id + "-points";
32789
+
32790
+ // When moving an element, set its aria-live to "polite" and the others
32791
+ // to "off". Otherwise, other connected elements that move at the same
32792
+ // time might override the currently focused element's aria live.
32793
+ const pointsOffArray = Array(points.length).fill("off");
32794
+ const [ariaLives, setAriaLives] = React__namespace.useState(pointsOffArray);
33006
32795
 
33007
32796
  // If the polygon is closed, return a LimitedPolygon component.
33008
32797
  if (closedPolygon) {
@@ -33017,7 +32806,22 @@ const UnlimitedPolygonGraph = statefulProps => {
33017
32806
  } = graphConfig;
33018
32807
  const widthPx = graphDimensionsInPixels[0];
33019
32808
  const heightPx = graphDimensionsInPixels[1];
33020
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
32809
+
32810
+ // Aria label srings
32811
+ const emptyGraph = coords.length === 0;
32812
+ const {
32813
+ srPolygonGraph,
32814
+ srPolygonGraphPointsNum,
32815
+ srPolygonGraphPoints
32816
+ } = describePolygonGraph(statefulProps.graphState, {
32817
+ strings,
32818
+ locale
32819
+ }, statefulProps.graphConfig.markings);
32820
+ return /*#__PURE__*/React__namespace.createElement("g", {
32821
+ // Outer graph minimal description
32822
+ "aria-label": emptyGraph ? strings.srUnlimitedPolygonEmpty : srPolygonGraph,
32823
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32824
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
33021
32825
  points: [...points],
33022
32826
  color: "var(--movable-line-stroke-color)",
33023
32827
  svgPolylineProps: {
@@ -33042,28 +32846,71 @@ const UnlimitedPolygonGraph = statefulProps => {
33042
32846
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
33043
32847
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
33044
32848
  }
33045
- }), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33046
- key: i,
33047
- point: point,
33048
- sequenceNumber: i + 1,
33049
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
33050
- ref: ref => {
33051
- pointsRef.current[i] = ref;
33052
- },
33053
- onFocus: () => {
33054
- dispatch(actions.polygon.focusPoint(i));
33055
- },
33056
- onClick: () => {
33057
- // If the point being clicked is the first point and
33058
- // there's enough non-duplicated points to form
33059
- // a polygon (3 or more), close the shape before
33060
- // setting focus.
33061
- if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
33062
- dispatch(actions.polygon.closePolygon());
33063
- }
33064
- dispatch(actions.polygon.clickPoint(i));
32849
+ }), coords.map((point, i) => {
32850
+ const angleId = `${id}-angle-${i}`;
32851
+ let sideIds = "";
32852
+
32853
+ // This point only has an angle if it's not the first
32854
+ // or last point in this open polygon.
32855
+ const hasAngle = i > 0 && i < coords.length - 1;
32856
+ const angle = hasAngle ? getAngleFromPoints(points, i) : null;
32857
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32858
+ const sidesArray = getSideLengthsFromPoints(points, i, true);
32859
+ for (let sideIndex = 0; sideIndex < sidesArray.length; sideIndex++) {
32860
+ sideIds += `${id}-point-${i}-side-${sideIndex} `;
33065
32861
  }
33066
- })));
32862
+ return /*#__PURE__*/React__namespace.createElement("g", {
32863
+ key: "point-" + i
32864
+ }, /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32865
+ ariaDescribedBy: `${angleId} ${sideIds}`,
32866
+ ariaLive: ariaLives[i],
32867
+ point: point,
32868
+ sequenceNumber: i + 1,
32869
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32870
+ ref: ref => {
32871
+ pointsRef.current[i] = ref;
32872
+ },
32873
+ onFocus: () => {
32874
+ dispatch(actions.polygon.focusPoint(i));
32875
+ const newPointAriaLives = [...pointsOffArray];
32876
+ newPointAriaLives[i] = "polite";
32877
+ // Whole polygon is "off", and the current
32878
+ // point is "polite".
32879
+ setAriaLives([...newPointAriaLives]);
32880
+ },
32881
+ onClick: () => {
32882
+ // If the point being clicked is the first point and
32883
+ // there's enough non-duplicated points to form
32884
+ // a polygon (3 or more), close the shape before
32885
+ // setting focus.
32886
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32887
+ dispatch(actions.polygon.closePolygon());
32888
+ }
32889
+ dispatch(actions.polygon.clickPoint(i));
32890
+ }
32891
+ }), angleDegree && /*#__PURE__*/React__namespace.createElement("g", {
32892
+ id: angleId
32893
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32894
+ angle: angleDegree
32895
+ }) : strings.srPolygonPointAngleApprox({
32896
+ angle: srFormatNumber(angleDegree, locale, 1)
32897
+ })), sidesArray.map((_ref2, j) => {
32898
+ let {
32899
+ pointIndex,
32900
+ sideLength
32901
+ } = _ref2;
32902
+ return /*#__PURE__*/React__namespace.createElement("g", {
32903
+ key: `${id}-point-${i}-side-${j}`,
32904
+ id: `${id}-point-${i}-side-${j}`
32905
+ }, getPolygonSideString(sideLength, pointIndex, strings, locale));
32906
+ }));
32907
+ }), coords.length > 0 && /*#__PURE__*/React__namespace.createElement("g", {
32908
+ id: polygonPointsNumId,
32909
+ style: a11y.srOnly
32910
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React__namespace.createElement("g", {
32911
+ id: polygonPointsId,
32912
+ style: a11y.srOnly
32913
+ }, srPolygonGraphPoints));
33067
32914
  };
33068
32915
  function getLines(points) {
33069
32916
  return points.map((point, i) => {
@@ -33082,6 +32929,54 @@ const hasFocusVisible = element => {
33082
32929
  return matches(":focus");
33083
32930
  }
33084
32931
  };
32932
+ function getPolygonGraphDescription(state, i18n, markings) {
32933
+ const strings = describePolygonGraph(state, i18n, markings);
32934
+ return strings.srPolygonInteractiveElements;
32935
+ }
32936
+ function describePolygonGraph(state, i18n, markings) {
32937
+ const {
32938
+ strings,
32939
+ locale
32940
+ } = i18n;
32941
+ const {
32942
+ coords
32943
+ } = state;
32944
+ const isCoordinatePlane = markings === "axes" || markings === "graph";
32945
+ const hasOnePoint = coords.length === 1;
32946
+
32947
+ // Figure out graph aria label based on markings.
32948
+ const srPolygonGraph = isCoordinatePlane ? strings.srPolygonGraphCoordinatePlane : strings.srPolygonGraph;
32949
+ const srPolygonGraphPointsNum = hasOnePoint ? strings.srPolygonGraphPointsOne : strings.srPolygonGraphPointsNum({
32950
+ num: coords.length
32951
+ });
32952
+ let srPolygonGraphPoints;
32953
+ // Figure out graph description based on markings.
32954
+ // If the graph is not on a coordinate plane, we should not include
32955
+ // the points' coordinates in the description.
32956
+ if (isCoordinatePlane) {
32957
+ const pointsString = coords.map((coord, i) => {
32958
+ return strings.srPointAtCoordinates({
32959
+ num: i + 1,
32960
+ x: srFormatNumber(coord[0], locale),
32961
+ y: srFormatNumber(coord[1], locale)
32962
+ });
32963
+ });
32964
+ srPolygonGraphPoints = pointsString.join(" ");
32965
+ }
32966
+ const srPolygonElementsNum = hasOnePoint ? strings.srPolygonElementsOne : strings.srPolygonElementsNum({
32967
+ num: coords.length
32968
+ });
32969
+ const srPolygonInteractiveElements = coords.length > 0 ? strings.srInteractiveElements({
32970
+ elements: [srPolygonElementsNum, srPolygonGraphPoints].join(" ")
32971
+ }) : null;
32972
+ return {
32973
+ srPolygonGraph,
32974
+ srPolygonGraphPointsNum,
32975
+ srPolygonGraphPoints,
32976
+ srPolygonElementsNum,
32977
+ srPolygonInteractiveElements
32978
+ };
32979
+ }
33085
32980
 
33086
32981
  function renderQuadraticGraph(state, dispatch, i18n) {
33087
32982
  return {
@@ -33788,7 +33683,8 @@ const MafsGraph = props => {
33788
33683
  } = renderGraphElements({
33789
33684
  state,
33790
33685
  dispatch,
33791
- i18n
33686
+ i18n,
33687
+ markings: props.markings
33792
33688
  });
33793
33689
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
33794
33690
  value: {
@@ -33823,7 +33719,7 @@ const MafsGraph = props => {
33823
33719
  handleKeyboardEvent(event, state, dispatch);
33824
33720
  },
33825
33721
  "aria-label": fullGraphAriaLabel,
33826
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
33722
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && unlimitedGraphKeyboardPromptId),
33827
33723
  ref: graphRef,
33828
33724
  tabIndex: 0,
33829
33725
  onFocus: event => {
@@ -33849,7 +33745,19 @@ const MafsGraph = props => {
33849
33745
  bottom: 0,
33850
33746
  left: 0
33851
33747
  }
33852
- }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null)), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
33748
+ }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null)), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
33749
+ // If we have locked figures, we set aria-hidden
33750
+ // to false so that screen readers can read the
33751
+ // locked figures. If we don't have any locked
33752
+ // figures, we need to set aria-hidden to true so
33753
+ // that screen readers don't read this Mafs element
33754
+ // as an empty image.
33755
+ // Note: Adding this in a wrapping View element
33756
+ // so that `aria-hidden` is a valid property.
33757
+ // It does not work on `Mafs` or svg elements.
33758
+ , {
33759
+ "aria-hidden": props.lockedFigures && props.lockedFigures.length > 0 ? false : true
33760
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
33853
33761
  preserveAspectRatio: false,
33854
33762
  viewBox: {
33855
33763
  x: state.range[X],
@@ -33869,10 +33777,10 @@ const MafsGraph = props => {
33869
33777
  height: height
33870
33778
  })),
33871
33779
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
33872
- (props.markings === "graph" || props.markings === "axes") && /*#__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, {
33780
+ (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), props.lockedFigures && props.lockedFigures.length > 0 && /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
33873
33781
  lockedFigures: props.lockedFigures,
33874
33782
  range: state.range
33875
- }))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
33783
+ })))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
33876
33784
  lockedFigures: props.lockedFigures
33877
33785
  }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
33878
33786
  style: {
@@ -34166,7 +34074,8 @@ const renderGraphElements = props => {
34166
34074
  const {
34167
34075
  state,
34168
34076
  dispatch,
34169
- i18n
34077
+ i18n,
34078
+ markings
34170
34079
  } = props;
34171
34080
  const {
34172
34081
  type
@@ -34183,7 +34092,7 @@ const renderGraphElements = props => {
34183
34092
  case "ray":
34184
34093
  return renderRayGraph(state, dispatch);
34185
34094
  case "polygon":
34186
- return renderPolygonGraph(state, dispatch);
34095
+ return renderPolygonGraph(state, dispatch, i18n, markings);
34187
34096
  case "point":
34188
34097
  return renderPointGraph(state, dispatch);
34189
34098
  case "circle":
@@ -35168,7 +35077,7 @@ const getChange = (coords, delta, constraintOpts) => {
35168
35077
  return [dx, dy];
35169
35078
  };
35170
35079
  // Less than or approximately equal
35171
- function leq$1(a, b) {
35080
+ function leq(a, b) {
35172
35081
  return a < b || perseusCore.approximateEqual(a, b);
35173
35082
  }
35174
35083
  function boundAndSnapToGrid(point, _ref) {
@@ -35339,7 +35248,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
35339
35248
 
35340
35249
  // Avoid degenerate triangles
35341
35250
  if (innerAngles.some(function (angle) {
35342
- return leq$1(angle, 1);
35251
+ return leq(angle, 1);
35343
35252
  })) {
35344
35253
  return startingPoint;
35345
35254
  }
@@ -35401,7 +35310,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
35401
35310
  });
35402
35311
 
35403
35312
  // Avoid degenerate triangles
35404
- if (leq$1(sides[1] + sides[2], sides[0]) || leq$1(sides[0] + sides[2], sides[1]) || leq$1(sides[0] + sides[1], sides[2])) {
35313
+ if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
35405
35314
  return startingPoint;
35406
35315
  }
35407
35316
 
@@ -35568,24 +35477,10 @@ const {
35568
35477
  sign,
35569
35478
  vector
35570
35479
  } = kmath.geometry;
35571
- const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
35572
35480
  const defaultBackgroundImage = {
35573
35481
  url: null
35574
35482
  };
35575
35483
  const UNLIMITED = "unlimited";
35576
-
35577
- // Sample background image:
35578
- // https://ka-perseus-graphie.s3.amazonaws.com/29c1b0fcd17fe63df0f148fe357044d5d5c7d0bb.png
35579
-
35580
- // default to defaultValue if actual is null or undefined
35581
- function defaultVal(actual, defaultValue) {
35582
- return actual == null ? defaultValue : actual;
35583
- }
35584
-
35585
- // Less than or approximately equal
35586
- function leq(a, b) {
35587
- return a < b || perseusCore.approximateEqual(a, b);
35588
- }
35589
35484
  function capitalize(str) {
35590
35485
  return str.replace(/(?:^|-)(.)/g, function (match, letter) {
35591
35486
  return letter.toUpperCase();
@@ -35594,14 +35489,6 @@ function capitalize(str) {
35594
35489
  function numSteps(range, step) {
35595
35490
  return Math.floor((range[1] - range[0]) / step);
35596
35491
  }
35597
- const _getShouldShowInstructions = props => {
35598
- return _isClickToAddPoints(props) && (
35599
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35600
- props.graph.coords == null || props.graph.coords.length === 0);
35601
- };
35602
- const _isClickToAddPoints = props => {
35603
- return props.graph.type === "point" && props.graph.numPoints === UNLIMITED || props.graph.type === "polygon" && props.graph.numSides === UNLIMITED;
35604
- };
35605
35492
  const makeInvalidTypeError = (functionName, graphType) => {
35606
35493
  return new perseusCore.PerseusError(`${functionName} called but current graph type is not a '${graphType}'`, perseusCore.Errors.NotAllowed, {
35607
35494
  metadata: {
@@ -35642,1200 +35529,6 @@ function getQuadraticCoefficients(coords) {
35642
35529
  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;
35643
35530
  return [a, b, c];
35644
35531
  }
35645
-
35646
- // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
35647
- // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
35648
- // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
35649
- // Once the legacy graph is removed, we should move this logic directly into the validate function below.
35650
- class LegacyInteractiveGraph extends React__namespace.Component {
35651
- static contextType = PerseusI18nContext;
35652
- angle;
35653
- circle;
35654
- graphie;
35655
- horizHairline;
35656
- line;
35657
- lines;
35658
- parabola;
35659
- pointA;
35660
- pointB;
35661
- pointC;
35662
- // @ts-expect-error - TS2564 - Property 'points' has no initializer and is not definitely assigned in the constructor.
35663
- points;
35664
- polygon;
35665
- // @ts-expect-error - TS2564 - Property 'shouldResetGraphie' has no initializer and is not definitely assigned in the constructor.
35666
- shouldResetGraphie;
35667
- sinusoid;
35668
- trashCan;
35669
- vertHairline;
35670
- static defaultProps = {
35671
- labels: ["x", "y"],
35672
- range: [[-10, 10], [-10, 10]],
35673
- step: [1, 1],
35674
- backgroundImage: defaultBackgroundImage,
35675
- markings: "graph",
35676
- showTooltips: false,
35677
- showProtractor: false,
35678
- graph: {
35679
- type: "linear"
35680
- }
35681
- };
35682
- state = {
35683
- shouldShowInstructions: _getShouldShowInstructions(this.props)
35684
- };
35685
- componentDidMount() {
35686
- if (this.refs.graph) {
35687
- // eslint-disable-next-line react/no-string-refs
35688
- // @ts-expect-error - TS2339 - Property 'graphie' does not exist on type 'ReactInstance'.
35689
- this.setGraphie(this.refs.graph.graphie());
35690
- }
35691
- }
35692
- UNSAFE_componentWillReceiveProps(nextProps) {
35693
- if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) {
35694
- this.shouldResetGraphie = true;
35695
- this.setState({
35696
- shouldShowInstructions: _getShouldShowInstructions(nextProps)
35697
- });
35698
- }
35699
- if (this.props.backgroundImage?.url !== nextProps.backgroundImage?.url || this.props.backgroundImage !== nextProps.backgroundImage || this.props.containerSizeClass !== nextProps.containerSizeClass) {
35700
- this.shouldResetGraphie = true;
35701
- }
35702
- }
35703
- componentDidUpdate(prevProps, prevState) {
35704
- // TODO(jeremy): This feels underspecified. For example, if the props
35705
- // change the coords of a segment graph, but not the numSegments, we'd
35706
- // not count that as an update.
35707
- const oldType = prevProps.graph.type;
35708
- const newType = this.props.graph.type;
35709
- if (oldType !== newType || prevProps.graph.type === "angle" && this.props.graph.type === "angle" && this.areAngleGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "point" && this.props.graph.type === "point" && this.arePointGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "polygon" && this.props.graph.type === "polygon" && this.arePolygonGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "segment" && this.props.graph.type === "segment" && this.areSegmentGraphsEqual(prevProps.graph, this.props.graph)) {
35710
- this["remove" + capitalize(oldType) + "Controls"]();
35711
- this["add" + capitalize(newType) + "Controls"]();
35712
- }
35713
- if (this.shouldResetGraphie) {
35714
- this.resetGraphie();
35715
- }
35716
- }
35717
- _getShouldShowInstructions = props => {
35718
- props = props || this.props;
35719
- return this.isClickToAddPoints(props) && (
35720
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35721
- props.graph.coords == null || props.graph.coords.length === 0);
35722
- };
35723
- setGraphie = newGraphie => {
35724
- this.graphie = newGraphie;
35725
- this.setupGraphie();
35726
- };
35727
- handleAddPointsMouseDown = coord => {
35728
- // This function should only be called when this.isClickToAddPoints()
35729
- // is true
35730
- if (!this.isClickToAddPoints()) {
35731
- throw new perseusCore.PerseusError("handleAddPointsClick should not be registered" + "when isClickToAddPoints() is false", perseusCore.Errors.NotAllowed);
35732
- }
35733
- if (!this.isCoordInTrash(coord)) {
35734
- let point;
35735
- if (this.props.graph.type === "point") {
35736
- point = this.createPointForPointsType(coord, this.points.length);
35737
- if (!point.constrain()) {
35738
- point.remove();
35739
- return;
35740
- }
35741
- this.points.push(point);
35742
-
35743
- // interactive2 allows us to grab the point
35744
- const idx = this.points.length - 1;
35745
- this.points[idx].grab(coord);
35746
- this.updateCoordsFromPoints();
35747
- } else if (this.props.graph.type === "polygon") {
35748
- if (this.polygon?.closed()) {
35749
- return;
35750
- }
35751
- point = this.createPointForPolygonType(coord, this.points.length);
35752
- this.points.push(point);
35753
- const idx = this.points.length - 1;
35754
- this.points[idx].grab(coord);
35755
-
35756
- // We don't call updateCoordsFromPoints for
35757
- // polygons, since the polygon won't be
35758
- // closed yet.
35759
- this.updatePolygon();
35760
- }
35761
- this.setState({
35762
- shouldShowInstructions: false
35763
- });
35764
- }
35765
- };
35766
- resetGraphie = () => {
35767
- this.shouldResetGraphie = false;
35768
- this.parabola = null;
35769
- this.sinusoid = null;
35770
- // eslint-disable-next-line react/no-string-refs
35771
- // @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'.
35772
- this.refs.graph?.reset();
35773
- };
35774
- setupGraphie = () => {
35775
- this.setTrashCanVisibility(0);
35776
- if (this.isClickToAddPoints()) {
35777
- this.setTrashCanVisibility(0.5);
35778
- }
35779
- if (this.props.apiOptions.isMobile) {
35780
- this.horizHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
35781
- normalStyle: {
35782
- strokeWidth: 1
35783
- }
35784
- });
35785
- this.horizHairline.attr({
35786
- stroke: KhanColors.INTERACTIVE
35787
- });
35788
- this.horizHairline?.hide();
35789
- this.vertHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
35790
- normalStyle: {
35791
- strokeWidth: 1
35792
- }
35793
- });
35794
- this.vertHairline.attr({
35795
- stroke: KhanColors.INTERACTIVE
35796
- });
35797
- this.vertHairline?.hide();
35798
- }
35799
- const type = this.props.graph.type;
35800
- this["add" + capitalize(type) + "Controls"]();
35801
- };
35802
- showHairlines = point => {
35803
- if (this.props.apiOptions.isMobile && this.props.markings !== "none") {
35804
- // Hairlines are already initialized when the graph is loaded, so
35805
- // here we just move them to the updated location and make them
35806
- // visible.
35807
- this.horizHairline?.moveTo([this.props.range[0][0], point[1]], [this.props.range[0][1], point[1]]);
35808
- this.horizHairline?.show();
35809
- this.vertHairline?.moveTo([point[0], this.props.range[1][0]], [point[0], this.props.range[1][1]]);
35810
- this.vertHairline?.show();
35811
- }
35812
- };
35813
- hideHairlines = () => {
35814
- if (this.props.apiOptions.isMobile) {
35815
- this.horizHairline?.hide();
35816
- this.vertHairline?.hide();
35817
- }
35818
- };
35819
- setTrashCanVisibility = opacity => {
35820
- const graphie = this.graphie;
35821
- if (kmath.number.equal(opacity, 0)) {
35822
- if (this.trashCan) {
35823
- this.trashCan.remove();
35824
- this.trashCan = null;
35825
- }
35826
- } else if (!this.props.apiOptions.isMobile) {
35827
- // Only if trash tooltips are not being used, we initialize the old
35828
- // trash can area.
35829
- if (!this.trashCan) {
35830
- this.trashCan = graphie?.raphael?.image(TRASH_ICON_URI, graphie.xpixels - 40, graphie.ypixels - 40, 40, 40);
35831
- }
35832
- this.trashCan?.attr({
35833
- opacity: opacity
35834
- });
35835
- }
35836
- };
35837
- isClickToAddPoints = props => {
35838
- props = props || this.props;
35839
- return _isClickToAddPoints(props);
35840
- };
35841
- areAngleGraphsEqual(prevGraph, currentGraph) {
35842
- return prevGraph.allowReflexAngles !== currentGraph.allowReflexAngles || prevGraph.angleOffsetDeg !== currentGraph.angleOffsetDeg || prevGraph.snapDegrees !== currentGraph.snapDegrees;
35843
- }
35844
- arePointGraphsEqual(prevGraph, currentGraph) {
35845
- return prevGraph.numPoints !== currentGraph.numPoints;
35846
- }
35847
- arePolygonGraphsEqual(prevGraph, currentGraph) {
35848
- return prevGraph.numSides !== currentGraph.numSides || prevGraph.showAngles !== currentGraph.showAngles || prevGraph.showSides !== currentGraph.showSides || prevGraph.snapTo !== currentGraph.snapTo;
35849
- }
35850
- areSegmentGraphsEqual(prevGraph, currentGraph) {
35851
- return prevGraph.numSegments !== currentGraph.numSegments;
35852
- }
35853
- _lineStroke = () => {
35854
- // This should probably use: this.props.apiOptions.isMobile
35855
- // @ts-expect-error - TS2339 - Property 'isMobile' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
35856
- return this.props.isMobile ? {
35857
- "stroke-width": 3
35858
- } : {};
35859
- };
35860
- addLine = type => {
35861
- const self = this;
35862
- const graphie = self.graphie;
35863
- const coords = InteractiveGraph.getLineCoords(self.props.graph, self.props);
35864
- const points = self.points = ___default["default"].map(coords, coord => {
35865
- return Interactive2.addMaybeMobileMovablePoint(this, {
35866
- coord: coord,
35867
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap()],
35868
- onMove: () => {
35869
- const graph = ___default["default"].extend({}, self.props.graph, {
35870
- coords: ___default["default"].invoke(points, "coord")
35871
- });
35872
- self.onChange({
35873
- graph: graph
35874
- });
35875
- }
35876
- });
35877
- });
35878
- const lineConfig = {
35879
- points: points,
35880
- static: true,
35881
- normalStyle: {
35882
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
35883
- ...this._lineStroke()
35884
- }
35885
- };
35886
- if (type === "line") {
35887
- // @ts-expect-error - TS2339 - Property 'extendLine' does not exist on type '{ readonly points: any[]; readonly static: true; readonly normalStyle: { readonly "stroke-width"?: number | undefined; readonly stroke: "#71B307" | "#63D9EA"; }; }'.
35888
- lineConfig.extendLine = true;
35889
- } else if (type === "ray") {
35890
- // @ts-expect-error - TS2339 - Property 'extendRay' does not exist on type '{ readonly points: any[]; readonly static: true; readonly normalStyle: { readonly "stroke-width"?: number | undefined; readonly stroke: "#71B307" | "#63D9EA"; }; }'.
35891
- lineConfig.extendRay = true;
35892
- }
35893
- self.line = Interactive2.addMovableLine(graphie, lineConfig);
35894
-
35895
- // A and B can't be in the same place
35896
- points[0].listen("constraints", "isLine", coord => {
35897
- return !kmath.point.equal(coord, points[1].coord());
35898
- });
35899
- points[1].listen("constraints", "isLine", coord => {
35900
- return !kmath.point.equal(coord, points[0].coord());
35901
- });
35902
- };
35903
- removeLine = () => {
35904
- ___default["default"].invoke(this.points, "remove");
35905
- this.line?.remove();
35906
- };
35907
- addNoneControls = () => {};
35908
- removeNoneControls = () => {};
35909
- addLinearControls = () => {
35910
- this.addLine("line");
35911
- };
35912
- removeLinearControls = () => {
35913
- this.removeLine();
35914
- };
35915
- addQuadraticControls = () => {
35916
- if (this.props.graph.type !== "quadratic") {
35917
- throw makeInvalidTypeError("addQuadraticControls", "quadratic");
35918
- }
35919
- let coords = this.props.graph.coords;
35920
- if (!coords) {
35921
- coords = InteractiveGraph.defaultQuadraticCoords(this.props);
35922
- }
35923
- const onMoveHandler = () => {
35924
- const graph = ___default["default"].extend({}, this.props.graph, {
35925
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'.
35926
- coords: [pointA.coord(), pointB.coord(), pointC.coord()]
35927
- });
35928
- this.onChange({
35929
- graph
35930
- });
35931
- this.updateQuadratic();
35932
- };
35933
-
35934
- // NOTE(jeresig): This code attempts to access the variable while it's
35935
- // still being defined, it should be refactored! The callbacks appear
35936
- // to be executed synchronously, which causes this issue.
35937
- let pointA = null;
35938
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
35939
- coord: coords[0],
35940
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35941
- return !pointA ||
35942
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35943
- coord[0] !== pointB.coord()[0] &&
35944
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35945
- coord[0] !== pointC.coord()[0];
35946
- }],
35947
- onMove: onMoveHandler
35948
- });
35949
-
35950
- // NOTE(jeresig): This code attempts to access the variable while it's
35951
- // still being defined, it should be refactored! The callbacks appear
35952
- // to be executed synchronously, which causes this issue.
35953
- let pointB = null;
35954
- pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
35955
- coord: coords[1],
35956
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35957
- return !pointB ||
35958
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35959
- coord[0] !== pointA.coord()[0] &&
35960
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35961
- coord[0] !== pointC.coord()[0];
35962
- }],
35963
- onMove: onMoveHandler
35964
- });
35965
-
35966
- // NOTE(jeresig): This code attempts to access the variable while it's
35967
- // still being defined, it should be refactored! The callbacks appear
35968
- // to be executed synchronously, which causes this issue.
35969
- let pointC = null;
35970
- pointC = this.pointC = Interactive2.addMaybeMobileMovablePoint(this, {
35971
- coord: coords[2],
35972
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35973
- return !pointC ||
35974
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35975
- coord[0] !== pointA.coord()[0] &&
35976
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35977
- coord[0] !== pointB.coord()[0];
35978
- }],
35979
- onMove: onMoveHandler
35980
- });
35981
- this.updateQuadratic();
35982
- };
35983
- updateQuadratic = () => {
35984
- const coeffs = InteractiveGraph.getCurrentQuadraticCoefficients(this.props);
35985
- if (!coeffs) {
35986
- return;
35987
- }
35988
-
35989
- // Extract coefficients the parabola
35990
- const a = coeffs[0];
35991
- const b = coeffs[1];
35992
- const c = coeffs[2];
35993
-
35994
- // Plot and style
35995
- if (this.parabola) {
35996
- const path = this.graphie?.svgParabolaPath(a, b, c);
35997
- this.parabola?.attr({
35998
- path: path
35999
- });
36000
- } else {
36001
- this.parabola = this.graphie?.parabola(a, b, c);
36002
- this.parabola?.attr({
36003
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC,
36004
- ...this._lineStroke()
36005
- });
36006
- this.parabola?.toBack();
36007
- }
36008
- };
36009
- removeQuadraticControls = () => {
36010
- this.pointA?.remove();
36011
- this.pointB?.remove();
36012
- this.pointC?.remove();
36013
- if (this.parabola) {
36014
- this.parabola.remove();
36015
- this.parabola = null;
36016
- }
36017
- };
36018
- addSinusoidControls = () => {
36019
- if (this.props.graph.type !== "sinusoid") {
36020
- throw makeInvalidTypeError("addSinusoidControls", "sinusoid");
36021
- }
36022
- let coords = this.props.graph.coords;
36023
- if (!coords) {
36024
- coords = InteractiveGraph.defaultSinusoidCoords(this.props);
36025
- }
36026
- const onMoveHandler = () => {
36027
- const graph = ___default["default"].extend({}, this.props.graph, {
36028
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
36029
- coords: [pointA.coord(), pointB.coord()]
36030
- });
36031
- this.onChange({
36032
- graph: graph
36033
- });
36034
- this.updateSinusoid();
36035
- };
36036
-
36037
- // NOTE(jeresig): This code attempts to access the variable while it's
36038
- // still being defined, it should be refactored! The callbacks appear
36039
- // to be executed synchronously, which causes this issue.
36040
- let pointA = null;
36041
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
36042
- coord: coords[0],
36043
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
36044
- return !pointA || coord[0] !== pointB.coord()[0];
36045
- }],
36046
- onMove: onMoveHandler
36047
- });
36048
- const pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
36049
- coord: coords[1],
36050
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
36051
- // @ts-expect-error - TS2339 - Property 'coord' does not exist on type 'never'.
36052
- return !pointA || coord[0] !== pointA.coord()[0];
36053
- }],
36054
- onMove: onMoveHandler
36055
- });
36056
- this.updateSinusoid();
36057
- };
36058
- updateSinusoid = () => {
36059
- const coeffs = InteractiveGraph.getCurrentSinusoidCoefficients(this.props);
36060
- if (!coeffs) {
36061
- return;
36062
- }
36063
- const a = coeffs[0];
36064
- const b = coeffs[1];
36065
- const c = coeffs[2];
36066
- const d = coeffs[3];
36067
-
36068
- // Plot and style
36069
- if (this.sinusoid) {
36070
- const path = this.graphie?.svgSinusoidPath(a, b, c, d);
36071
- this.sinusoid?.attr({
36072
- path: path
36073
- });
36074
- } else {
36075
- this.sinusoid = this.graphie?.sinusoid(a, b, c, d);
36076
- this.sinusoid?.attr({
36077
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC,
36078
- ...this._lineStroke()
36079
- });
36080
- this.sinusoid?.toBack();
36081
- }
36082
- };
36083
- removeSinusoidControls = () => {
36084
- this.pointA?.remove();
36085
- this.pointB?.remove();
36086
- if (this.sinusoid) {
36087
- this.sinusoid.remove();
36088
- this.sinusoid = null;
36089
- }
36090
- };
36091
- addCircleControls = () => {
36092
- const graphie = this.graphie;
36093
- const minSnap = ___default["default"].min(graphie.snap);
36094
- const circle = this.circle = graphie?.addCircleGraph({
36095
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'.
36096
- center: this.props.graph.center || [0, 0],
36097
- // @ts-expect-error - TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
36098
- radius: this.props.graph.radius || ___default["default"].min(this.props.step),
36099
- snapX: graphie.snap[0],
36100
- snapY: graphie.snap[1],
36101
- minRadius: minSnap,
36102
- snapRadius: minSnap
36103
- });
36104
- $__default["default"](circle).on("move", () => {
36105
- const graph = ___default["default"].extend({}, this.props.graph, {
36106
- center: circle.center,
36107
- radius: circle.radius
36108
- });
36109
- this.onChange({
36110
- graph: graph
36111
- });
36112
- });
36113
- };
36114
- removeCircleControls = () => {
36115
- this.circle?.remove();
36116
- };
36117
- addLinearSystemControls = () => {
36118
- const graphie = this.graphie;
36119
- const coords = InteractiveGraph.getLinearSystemCoords(this.props.graph, this.props);
36120
- const segmentColors = [KhanColors.INTERACTIVE, KhanColors.GREEN];
36121
- const points = this.points = ___default["default"].map(coords, (segmentCoords, segmentIndex) => {
36122
- // NOTE(jeresig): This code attempts to access the variable
36123
- // while it's still being defined, it should be refactored!
36124
- // The callbacks appear to be executed synchronously, which
36125
- // causes this issue.
36126
- const segmentPoints = [];
36127
- for (let i = 0; i < segmentCoords.length; i += 1) {
36128
- const coord = segmentCoords[i];
36129
- segmentPoints.push(Interactive2.addMaybeMobileMovablePoint(this, {
36130
- coord: coord,
36131
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
36132
- const otherSegment = segmentPoints[1 - i];
36133
- if (!otherSegment) {
36134
- // segment hasn't been defined yet
36135
- // because we're still creating them
36136
- return;
36137
- }
36138
- return !kmath.point.equal(coord, otherSegment.coord());
36139
- }],
36140
- onMove: () => {
36141
- const graph = ___default["default"].extend({}, this.props.graph, {
36142
- coords: ___default["default"].map(this.points, segment => ___default["default"].invoke(segment, "coord"))
36143
- });
36144
- this.onChange({
36145
- graph: graph
36146
- });
36147
- },
36148
- normalStyle: {
36149
- fill: segmentColors[segmentIndex]
36150
- },
36151
- highlightStyle: {
36152
- fill: segmentColors[segmentIndex]
36153
- }
36154
- }));
36155
- }
36156
- return segmentPoints;
36157
- });
36158
- this.lines = ___default["default"].map(points, (segmentPoints, segmentIndex) => {
36159
- return Interactive2.addMovableLine(graphie, {
36160
- points: segmentPoints,
36161
- static: true,
36162
- extendLine: true,
36163
- normalStyle: {
36164
- stroke: segmentColors[segmentIndex]
36165
- }
36166
- });
36167
- });
36168
- };
36169
- removeLinearSystemControls = () => {
36170
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36171
- ___default["default"].invoke(this.lines, "remove");
36172
- ___default["default"].map(this.points, segment => ___default["default"].invoke(segment, "remove"));
36173
- };
36174
- isCoordInTrash = coord => {
36175
- if (this.props.apiOptions.isMobile) {
36176
- return false;
36177
- }
36178
- const graphie = this.graphie;
36179
- const screenPoint = graphie?.scalePoint(coord);
36180
- return screenPoint[0] >= graphie.xpixels - 40 && screenPoint[1] >= graphie.ypixels - 40;
36181
- };
36182
- createPointForPointsType = (coord, i) => {
36183
- const self = this;
36184
- const remove = () => {
36185
- self.points = ___default["default"].filter(self.points, function (pt) {
36186
- return pt !== point;
36187
- });
36188
- // update the correct answer box
36189
- self.updateCoordsFromPoints();
36190
-
36191
- // remove this movablePoint from graphie.
36192
- // we wait to do this until we're not inside of
36193
- // said point's onMoveEnd method so its state is
36194
- // consistent throughout this method call
36195
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
36196
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
36197
- setTimeout(point.remove.bind(point), 0); // eslint-disable-line no-restricted-syntax
36198
- };
36199
-
36200
- // NOTE(jeresig): This code attempts to access the variable while it's
36201
- // still being defined, it should be refactored! The callbacks appear
36202
- // to be executed synchronously, which causes this issue.
36203
- let point = null;
36204
- point = Interactive2.addMaybeMobileMovablePoint(this, {
36205
- coord: coord,
36206
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), function (coord) {
36207
- // TODO(jack): There ought to be a
36208
- // MovablePoint.constraints.avoid
36209
- // default that lets you do things like this
36210
- return ___default["default"].all(self.points, function (pt) {
36211
- return point === pt || !kmath.point.equal(coord, pt.coord());
36212
- });
36213
- }],
36214
- onMoveStart: function () {
36215
- if (self.isClickToAddPoints()) {
36216
- self.setTrashCanVisibility(1);
36217
- }
36218
- },
36219
- onMove: self.updateCoordsFromPoints,
36220
- onMoveEnd: function (coord) {
36221
- if (self.isClickToAddPoints()) {
36222
- if (self.isCoordInTrash(coord)) {
36223
- remove();
36224
- }
36225
- // In case we mouseup'd off the graphie and that
36226
- // stopped the move (in which case, we might not
36227
- // be in isCoordInTrash()
36228
- self.setTrashCanVisibility(0.5);
36229
- }
36230
- },
36231
- ...(this.props.apiOptions.isMobile && self.isClickToAddPoints() ? {
36232
- onRemove: remove
36233
- } : {})
36234
- });
36235
- return point;
36236
- };
36237
- removePoint = point => {
36238
- let index = null;
36239
- this.points = ___default["default"].filter(this.points, function (pt, i) {
36240
- if (pt === point) {
36241
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
36242
- index = i;
36243
- return false;
36244
- }
36245
- return true;
36246
- });
36247
- return index;
36248
- };
36249
- createPointForPolygonType = (coord, i) => {
36250
- if (this.props.graph.type !== "polygon") {
36251
- throw makeInvalidTypeError("createPointForPolygonType", "polygon");
36252
- }
36253
-
36254
- // TODO(alex): check against "grid" instead, use constants
36255
- const snapToGrid = !___default["default"].contains(["angles", "sides"], this.props.graph.snapTo);
36256
-
36257
- // Index relative to current point -> absolute index
36258
- // NOTE: This does not work when isClickToAddPoints() == true,
36259
- // as `i` can be changed by dragging a point to the trash
36260
- // Currently this function is only called when !isClickToAddPoints()
36261
- const rel = j => {
36262
- return (i + j + this.points.length) % this.points.length;
36263
- };
36264
- const remove = () => {
36265
- // remove this point from points
36266
- const index = this.removePoint(point);
36267
- if (this.polygon?.closed()) {
36268
- // We should be checking if this.points is defined before rotating them.
36269
- // @ts-expect-error - TS4104 - The type 'readonly any[]' is 'readonly' and cannot be assigned to the mutable type 'any[]'. | TS2345 - Argument of type 'number | null | undefined' is not assignable to parameter of type 'number | undefined'.
36270
- this.points = rotate(this.points, index);
36271
- this.polygon?.update({
36272
- closed: false
36273
- });
36274
- }
36275
- this.updatePolygon();
36276
- // the polygon is now unclosed, so we need to
36277
- // remove any points props
36278
- this.clearCoords();
36279
-
36280
- // remove this movablePoint from graphie.
36281
- // wait to do this until we're not inside of
36282
- // said point's onMoveEnd method so state is
36283
- // consistent throughout the method call
36284
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
36285
- // eslint-disable-next-line no-restricted-syntax
36286
- setTimeout(point.remove.bind(point), 0);
36287
- };
36288
- const setInitialMove = debounce__default["default"]((point, bool) => {
36289
- point.state.isInitialMove = bool;
36290
- }, 1000);
36291
- const onMoveEndHandler = coord => {
36292
- // If the user clicked and dragged a point over endpoint,
36293
- // join the them
36294
- const overlappedPoints = this.points.length > 1 && (point === this.points[0] && kmath.point.equal(coord, ___default["default"].last(this.points).coord()) || point === ___default["default"].last(this.points) && kmath.point.equal(coord, this.points[0].coord()));
36295
- if (this.isClickToAddPoints()) {
36296
- if (this.isCoordInTrash(coord)) {
36297
- remove();
36298
- } else if (overlappedPoints) {
36299
- const pointToRemove = this.points.pop();
36300
- if (this.points.length > 2) {
36301
- this.polygon?.update({
36302
- closed: true
36303
- });
36304
- this.updateCoordsFromPoints();
36305
- } else {
36306
- this.polygon?.update({
36307
- closed: false
36308
- });
36309
- this.clearCoords();
36310
- }
36311
- this.updatePolygon();
36312
- // remove this movablePoint from graphie.
36313
- // wait to do this until we're not inside of
36314
- // said point's onMoveEnd method so state is
36315
- // consistent throughout the method call
36316
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
36317
- // eslint-disable-next-line no-restricted-syntax
36318
- setTimeout(pointToRemove.remove.bind(pointToRemove), 0);
36319
- } else {
36320
- // If the user clicked and dragged a point over any other
36321
- // existing point, fix shape
36322
- const shouldRemove = ___default["default"].any(this.points, function (pt) {
36323
- return pt !== point && kmath.point.equal(pt.coord(), coord);
36324
- });
36325
- if (shouldRemove) {
36326
- this.removePoint(point);
36327
- if (this.points.length < 3) {
36328
- this.polygon?.update({
36329
- closed: false
36330
- });
36331
- this.clearCoords();
36332
- } else if (this.polygon?.closed()) {
36333
- this.updateCoordsFromPoints();
36334
- }
36335
- this.updatePolygon();
36336
- // remove this movablePoint from graphie.
36337
- // wait to do this until we're not inside
36338
- // said point's onMoveEnd method so state
36339
- // is consistent throughout the method call
36340
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
36341
- // eslint-disable-next-line no-restricted-syntax
36342
- setTimeout(point.remove.bind(point), 0);
36343
- } else {
36344
- // If this was
36345
- // * a click on the first or last point
36346
- // * and not a deletion
36347
- // * and not a drag,
36348
- // * and not a creation of a new point
36349
- // (see !point.state.isInitialMove, below),
36350
- // * and our polygon is not closed,
36351
- // * and we can close it (we need at least the same
36352
- // number of points as coordinates asked for in the
36353
- // question),
36354
- // then close it
36355
- const isFirstOrLastPoint = point === ___default["default"].head(this.points) || point === ___default["default"].last(this.points);
36356
- const pointNotMoved = !point.hasMoved();
36357
- // NOTE (jsatk): Currently different on mobile versus
36358
- // destkop
36359
- const notANewPoint = !point.state.isInitialMove;
36360
- const polygonNotClosed = !this.polygon?.closed();
36361
- const allPointsPlotted = this.points.length > 2;
36362
- const canCloseUnclosedPolygon = isFirstOrLastPoint && pointNotMoved && notANewPoint && polygonNotClosed && allPointsPlotted;
36363
- if (canCloseUnclosedPolygon) {
36364
- this.polygon?.update({
36365
- closed: true
36366
- });
36367
- this.updatePolygon();
36368
-
36369
- // We finally have a closed polygon, so save our
36370
- // points to props
36371
- this.updateCoordsFromPoints();
36372
- }
36373
- }
36374
- }
36375
-
36376
- // In case we mouseup'd off the graphie and that
36377
- // stopped the move
36378
- this.setTrashCanVisibility(0.5);
36379
- }
36380
- setInitialMove(point, false);
36381
- };
36382
- const graphConstraint = coord => {
36383
- // These constraints are all relative to the other points, so if
36384
- // we're creating the initial points and haven't added any others
36385
- // to the graph, we can't enforce them.
36386
- if (this.points == null || this.points.length === 0) {
36387
- return true;
36388
- }
36389
- const coords = ___default["default"].invoke(this.points, "coord");
36390
- coords[i] = coord;
36391
-
36392
- // Check for invalid positioning, but only if we aren't adding
36393
- // points one click at a time, since those added points could
36394
- // have already violated these constraints
36395
- if (!this.isClickToAddPoints()) {
36396
- // Polygons can't have consecutive collinear points
36397
- if (collinear(coords[rel(-2)], coords[rel(-1)], coords[i]) || collinear(coords[rel(-1)], coords[i], coords[rel(1)]) || collinear(coords[i], coords[rel(1)], coords[rel(2)])) {
36398
- return false;
36399
- }
36400
- const segments = ___default["default"].zip(coords, rotate(coords));
36401
- if (this.points.length > 3) {
36402
- // Constrain to simple (non self-intersecting) polygon by
36403
- // testing whether adjacent segments intersect any others
36404
- for (let j = -1; j <= 0; j++) {
36405
- const segment = segments[rel(j)];
36406
- const others = ___default["default"].without(segments, segment, segments[rel(j - 1)], segments[rel(j + 1)]);
36407
- for (let k = 0; k < others.length; k++) {
36408
- const other = others[k];
36409
- // @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Line'.
36410
- if (intersects(segment, other)) {
36411
- return false;
36412
- }
36413
- }
36414
- }
36415
- }
36416
- }
36417
- if (
36418
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
36419
- this.props.graph.snapTo === "angles" && this.points.length > 2) {
36420
- // Snap to whole degree interior angles
36421
-
36422
- const angles = ___default["default"].map(angleMeasures(coords), function (rad) {
36423
- return rad * 180 / Math.PI;
36424
- });
36425
- ___default["default"].each([-1, 1], function (j) {
36426
- angles[rel(j)] = Math.round(angles[rel(j)]);
36427
- });
36428
- const getAngle = function (a, vertex, b) {
36429
- const angle = GraphUtils.findAngleDeprecated(coords[rel(a)], coords[rel(b)], coords[rel(vertex)]);
36430
- return (angle + 360) % 360;
36431
- };
36432
- const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
36433
- innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
36434
-
36435
- // Avoid degenerate triangles
36436
- if (___default["default"].any(innerAngles, function (angle) {
36437
- return leq(angle, 1);
36438
- })) {
36439
- return false;
36440
- }
36441
- const knownSide = magnitude(
36442
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
36443
- vector(coords[rel(-1)], coords[rel(1)]));
36444
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
36445
-
36446
- // Solve for side by using the law of sines
36447
- const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
36448
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
36449
- const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
36450
- return this.graphie?.addPoints(coords[rel(-1)], offset);
36451
- }
36452
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
36453
- if (this.props.graph.snapTo === "sides" && this.points.length > 1) {
36454
- // Snap to whole unit side measures
36455
-
36456
- const sides = ___default["default"].map([[coords[rel(-1)], coords[i]], [coords[i], coords[rel(1)]], [coords[rel(-1)], coords[rel(1)]]], function (coords) {
36457
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
36458
- return magnitude(vector(...coords));
36459
- });
36460
- ___default["default"].each([0, 1], function (j) {
36461
- sides[j] = Math.round(sides[j]);
36462
- });
36463
-
36464
- // Avoid degenerate triangles
36465
- if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
36466
- return false;
36467
- }
36468
-
36469
- // Solve for angle by using the law of cosines
36470
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
36471
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
36472
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
36473
- const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
36474
- return this.graphie?.addPoints(coords[rel(-1)], offset);
36475
- }
36476
- // Snap to grid (already done)
36477
- return true;
36478
- };
36479
- const point = Interactive2.addMaybeMobileMovablePoint(this, {
36480
- coord: coord,
36481
- constraints: [Interactive2.MovablePoint.constraints.bound(), snapToGrid ? Interactive2.MovablePoint.constraints.snap() : null, graphConstraint],
36482
- onMoveStart: () => {
36483
- if (this.isClickToAddPoints()) {
36484
- this.setTrashCanVisibility(1);
36485
- }
36486
- },
36487
- onMove: () => {
36488
- if (this.polygon?.closed()) {
36489
- this.updateCoordsFromPoints();
36490
- }
36491
- },
36492
- onMoveEnd: onMoveEndHandler,
36493
- ...(this.props.apiOptions.isMobile && this.isClickToAddPoints() ? {
36494
- onRemove: remove
36495
- } : {})
36496
- });
36497
- point.state.isInitialMove = true;
36498
- return point;
36499
- };
36500
- updateCoordsFromPoints = () => {
36501
- const graph = ___default["default"].extend({}, this.props.graph, {
36502
- // Handle old movable points with .coord, or
36503
- // Interactive2.MovablePoint's with .coord()
36504
- coords: ___default["default"].map(this.points, function (point) {
36505
- return ___default["default"].result(point, "coord");
36506
- })
36507
- });
36508
- this.onChange({
36509
- graph: graph
36510
- });
36511
- };
36512
- clearCoords = () => {
36513
- const graph = ___default["default"].extend({}, this.props.graph, {
36514
- coords: null
36515
- });
36516
- this.onChange({
36517
- graph: graph
36518
- });
36519
- };
36520
- onChange = data => {
36521
- this.props.onChange(data);
36522
- this.props.trackInteraction();
36523
- };
36524
- addPointControls = () => {
36525
- if (this.props.graph.type !== "point") {
36526
- throw makeInvalidTypeError("addPointControls", "point");
36527
- }
36528
- const coords = InteractiveGraph.getPointCoords(this.props.graph, this.props);
36529
- // Clear out our old points so that newly added points don't
36530
- // "collide" with them and reposition when being added
36531
- // Without this, when added, each point checks whether it is on top
36532
- // of a point in this.points, which (a) shouldn't matter since
36533
- // we're clearing out this.points anyways, and (b) can cause problems
36534
- // if each of this.points is a MovablePoint instead of an
36535
- // Interactive2.MovablePoint, since one has a .coord and the other
36536
- // has .coord()
36537
- // TODO(jack): Figure out a better way to do this
36538
- this.points = [];
36539
- this.points = ___default["default"].map(coords, this.createPointForPointsType, this);
36540
- };
36541
- removePointControls = () => {
36542
- ___default["default"].invoke(this.points, "remove");
36543
- };
36544
- addSegmentControls = () => {
36545
- if (this.props.graph.type !== "segment") {
36546
- throw makeInvalidTypeError("addSegmentControls", "segment");
36547
- }
36548
- const self = this;
36549
- const graphie = this.graphie;
36550
- const coords = InteractiveGraph.getSegmentCoords(this.props.graph, this.props);
36551
- const createPoint = options => Interactive2.addMaybeMobileMovablePoint(this, options);
36552
- this.points = [];
36553
- this.lines = ___default["default"].map(coords, function (segment, i) {
36554
- const updateCoordProps = function () {
36555
- const graph = ___default["default"].extend({}, self.props.graph, {
36556
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36557
- coords: ___default["default"].invoke(self.lines, "coords")
36558
- });
36559
- self.onChange({
36560
- graph: graph
36561
- });
36562
- };
36563
-
36564
- // NOTE(jeresig): This code attempts to access the variable
36565
- // while it's still being defined, it should be refactored!
36566
- // The callbacks appear to be executed synchronously, which
36567
- // causes this issue.
36568
- const points = [];
36569
- for (let i = 0; i < segment.length; i += 1) {
36570
- const coord = segment[i];
36571
- points.push(createPoint({
36572
- coord: coord,
36573
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
36574
- const otherPoint = points[1 - i];
36575
- if (!otherPoint) {
36576
- // point hasn't been defined yet
36577
- // because we're still creating them
36578
- // We return false as the points aren't
36579
- // going to be in the same position at
36580
- // this point.
36581
- return false;
36582
- }
36583
- return !kmath.point.equal(coord, otherPoint.coord());
36584
- }],
36585
- onMove: updateCoordProps
36586
- }));
36587
- }
36588
- self.points = self.points.concat(points);
36589
- const line = Interactive2.addMovableLine(graphie, {
36590
- points: points,
36591
- static: false,
36592
- constraints: [
36593
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36594
- Interactive2.MovableLine.constraints.bound(),
36595
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36596
- Interactive2.MovableLine.constraints.snap()],
36597
- onMove: [
36598
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36599
- Interactive2.MovableLine.onMove.updatePoints, updateCoordProps],
36600
- normalStyle: {
36601
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36602
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
36603
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36604
- ...this._lineStroke()
36605
- },
36606
- highlightStyle: {
36607
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36608
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTING,
36609
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36610
- ...this._lineStroke()
36611
- }
36612
- });
36613
- ___default["default"].invoke(points, "toFront");
36614
- return line;
36615
- }, this);
36616
- };
36617
- removeSegmentControls = () => {
36618
- ___default["default"].invoke(this.points, "remove");
36619
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36620
- ___default["default"].invoke(this.lines, "remove");
36621
- };
36622
- addRayControls = () => {
36623
- this.addLine("ray");
36624
- };
36625
- removeRayControls = () => {
36626
- this.removeLine();
36627
- };
36628
- addPolygonControls = () => {
36629
- this.polygon = null;
36630
- const coords = InteractiveGraph.getPolygonCoords(this.props.graph, this.props);
36631
- // Clear out our old points so that newly added points don't
36632
- // "collide", as in `addPointControls`
36633
- this.points = [];
36634
- this.points = ___default["default"].map(coords, this.createPointForPolygonType, this);
36635
- this.updatePolygon();
36636
- };
36637
- updatePolygon = () => {
36638
- if (this.props.graph.type !== "polygon") {
36639
- throw makeInvalidTypeError("updatePolygon", "polygon");
36640
- }
36641
- let closed;
36642
- if (this.polygon) {
36643
- closed = this.polygon.closed();
36644
- } else if (this.points.length >= 3) {
36645
- closed = true;
36646
- } else {
36647
- // There will only be fewer than 3 points in click-to-add-vertices
36648
- // mode, so we don't need to explicitly check for that here.
36649
- closed = false;
36650
- }
36651
- const graphie = this.graphie;
36652
- const n = this.points.length;
36653
-
36654
- // TODO(alex): check against "grid" instead, use constants
36655
- const snapToGrid = !___default["default"].contains(["angles", "sides"], this.props.graph.snapTo);
36656
- const angleLabels = ___default["default"].times(n, function (i) {
36657
- if (
36658
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36659
- !this.props.graph.showAngles || !closed && (i === 0 || i === n - 1)) {
36660
- return "";
36661
- }
36662
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36663
- if (this.props.graph.snapTo === "angles") {
36664
- return "$deg0";
36665
- }
36666
- return "$deg1";
36667
- }, this);
36668
- const showRightAngleMarkers = ___default["default"].times(n, function (i) {
36669
- return closed || i !== 0 && i !== n - 1;
36670
- }, this);
36671
- const numArcs = ___default["default"].times(n, function (i) {
36672
- if (
36673
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36674
- this.props.graph.showAngles && (closed || i !== 0 && i !== n - 1)) {
36675
- return 1;
36676
- }
36677
- return 0;
36678
- }, this);
36679
- const sideLabels = ___default["default"].times(n, function (i) {
36680
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36681
- if (!this.props.graph.showSides || !closed && i === n - 1) {
36682
- return "";
36683
- }
36684
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36685
- if (this.props.graph.snapTo === "sides") {
36686
- return "$len0";
36687
- }
36688
- return "$len1";
36689
- }, this);
36690
- if (this.polygon == null) {
36691
- const self = this;
36692
- self.polygon = Interactive2.addMovablePolygon(graphie, {
36693
- constraints: [
36694
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36695
- Interactive2.MovablePolygon.constraints.bound(), snapToGrid ?
36696
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36697
- Interactive2.MovablePolygon.constraints.snap() : null],
36698
- closed: closed,
36699
- points: self.points,
36700
- angleLabels: angleLabels,
36701
- showRightAngleMarkers: showRightAngleMarkers,
36702
- numArcs: numArcs,
36703
- sideLabels: sideLabels,
36704
- onMove: [
36705
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36706
- Interactive2.MovablePolygon.onMove.updatePoints, function () {
36707
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36708
- if (this.closed()) {
36709
- self.updateCoordsFromPoints();
36710
- }
36711
- }],
36712
- normalStyle: {
36713
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
36714
- ...this._lineStroke()
36715
- }
36716
- });
36717
- } else {
36718
- // We only need to pass in the properties that might've changed
36719
- this.polygon.update({
36720
- closed: closed,
36721
- points: this.points,
36722
- angleLabels: angleLabels,
36723
- showRightAngleMarkers: showRightAngleMarkers,
36724
- numArcs: numArcs,
36725
- sideLabels: sideLabels
36726
- });
36727
- }
36728
- };
36729
- removePolygonControls = () => {
36730
- ___default["default"].invoke(this.points, "remove");
36731
- this.polygon?.remove();
36732
- };
36733
- addAngleControls = () => {
36734
- if (this.props.graph.type !== "angle") {
36735
- throw makeInvalidTypeError("addAngleControls", "angle");
36736
- }
36737
- const graph = this.props.graph;
36738
- const graphie = this.graphie;
36739
- const coords = InteractiveGraph.getAngleCoords(this.props.graph, this.props);
36740
-
36741
- // The vertex snaps to the grid, but the rays don't...
36742
- this.points = coords.map(function (coord, i) {
36743
- return graphie?.addMovablePoint(___default["default"].extend({
36744
- coord: coord,
36745
- normalStyle: {
36746
- stroke: KhanColors.INTERACTIVE,
36747
- fill: KhanColors.INTERACTIVE
36748
- }
36749
- }, i === 1 ? {
36750
- snapX: graphie.snap[0],
36751
- snapY: graphie.snap[1]
36752
- } : {}));
36753
- });
36754
-
36755
- // ...they snap to whole-degree angles from the vertex.
36756
- this.angle = graphie?.addMovableAngle({
36757
- points: this.points,
36758
- snapDegrees: graph.snapDegrees || 1,
36759
- snapOffsetDeg: graph.angleOffsetDeg || 0,
36760
- angleLabel: graph.showAngles ? "$deg0" : "",
36761
- pushOut: 2,
36762
- allowReflex: defaultVal(graph.allowReflexAngles, true)
36763
- });
36764
- $__default["default"](this.angle).on("move", () => {
36765
- this.onChange({
36766
- graph: {
36767
- ...graph,
36768
- coords: this.angle?.getClockwiseCoords()
36769
- }
36770
- });
36771
- });
36772
- };
36773
- removeAngleControls = () => {
36774
- ___default["default"].invoke(this.points, "remove");
36775
- this.angle?.remove();
36776
- };
36777
- toggleShowSides = () => {
36778
- if (this.props.graph.type !== "polygon") {
36779
- throw makeInvalidTypeError("toggleShowSides", "polygon");
36780
- }
36781
- const graph = ___default["default"].extend({}, this.props.graph, {
36782
- showSides: !this.props.graph.showSides
36783
- });
36784
- this.onChange({
36785
- graph: graph
36786
- });
36787
- };
36788
- getUserInput() {
36789
- return InteractiveGraph.getUserInputFromProps(this.props);
36790
- }
36791
- focus = $__default["default"].noop;
36792
- render() {
36793
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36794
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36795
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36796
- let instructions;
36797
- // isClickToAddPoints() only applies to points and polygons
36798
- if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
36799
- if (this.props.graph.type === "point") {
36800
- instructions = this.context.strings.addPoints;
36801
- } else if (this.props.graph.type === "polygon") {
36802
- instructions = this.context.strings.addVertices;
36803
- }
36804
- } else {
36805
- instructions = undefined;
36806
- }
36807
- let onMouseDown;
36808
- if (this.isClickToAddPoints()) {
36809
- onMouseDown = this.handleAddPointsMouseDown;
36810
- }
36811
- const isMobile = this.props.apiOptions.isMobile;
36812
- return /*#__PURE__*/React__namespace.createElement("div", {
36813
- className: "perseus-widget " + "perseus-widget-interactive-graph",
36814
- style: {
36815
- width: box[0],
36816
- height: box[1]
36817
- }
36818
- }, /*#__PURE__*/React__namespace.createElement(Graph, {
36819
- instructions: instructions
36820
- // eslint-disable-next-line react/no-string-refs
36821
- ,
36822
- ref: "graph",
36823
- box: box,
36824
- labels: this.props.labels,
36825
- range: this.props.range,
36826
- step: isMobile ? Util.constrainedTickStepsFromTickSteps(this.props.step, this.props.range) : this.props.step,
36827
- gridStep: gridStep,
36828
- snapStep: snapStep,
36829
- markings: this.props.markings,
36830
- backgroundImage: this.props.backgroundImage,
36831
- showProtractor: this.props.showProtractor,
36832
- onMouseDown: onMouseDown,
36833
- onGraphieUpdated: this.setGraphie,
36834
- setDrawingAreaAvailable: this.props.apiOptions.setDrawingAreaAvailable,
36835
- isMobile: isMobile
36836
- }));
36837
- }
36838
- }
36839
35532
  class InteractiveGraph extends React__namespace.Component {
36840
35533
  legacyGraphRef = /*#__PURE__*/React__namespace.createRef();
36841
35534
  mafsRef = /*#__PURE__*/React__namespace.createRef();
@@ -36864,25 +35557,17 @@ class InteractiveGraph extends React__namespace.Component {
36864
35557
  return getPromptJSON$b(this.props, this.getUserInput());
36865
35558
  }
36866
35559
  render() {
36867
- // Mafs shim
36868
- const mafsFlags = this.props.apiOptions?.flags?.["mafs"];
36869
- if (shouldUseMafs(mafsFlags, this.props.graph)) {
36870
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36871
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36872
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36873
- return /*#__PURE__*/React__namespace.createElement(StatefulMafsGraph, _extends({}, this.props, {
36874
- flags: this.props.apiOptions?.flags,
36875
- ref: this.mafsRef,
36876
- gridStep: gridStep,
36877
- snapStep: snapStep,
36878
- box: box,
36879
- showTooltips: !!this.props.showTooltips,
36880
- readOnly: this.props.apiOptions?.readOnly
36881
- }));
36882
- }
36883
- return /*#__PURE__*/React__namespace.createElement(LegacyInteractiveGraph, _extends({
36884
- ref: this.legacyGraphRef
36885
- }, this.props));
35560
+ const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
35561
+ const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
35562
+ const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
35563
+ return /*#__PURE__*/React__namespace.createElement(StatefulMafsGraph, _extends({}, this.props, {
35564
+ ref: this.mafsRef,
35565
+ gridStep: gridStep,
35566
+ snapStep: snapStep,
35567
+ box: box,
35568
+ showTooltips: !!this.props.showTooltips,
35569
+ readOnly: this.props.apiOptions?.readOnly
35570
+ }));
36886
35571
  }
36887
35572
 
36888
35573
  /**
@@ -37198,28 +35883,6 @@ class InteractiveGraph extends React__namespace.Component {
37198
35883
  }
37199
35884
  }
37200
35885
 
37201
- // exported for testing
37202
- function shouldUseMafs(mafsFlags, graph) {
37203
- if (typeof mafsFlags === "boolean" || typeof mafsFlags === "undefined") {
37204
- return false;
37205
- }
37206
- switch (graph.type) {
37207
- case "none":
37208
- return true;
37209
- case "point":
37210
- if (graph.numPoints === UNLIMITED) {
37211
- return Boolean(mafsFlags["unlimited-point"]);
37212
- }
37213
- return Boolean(mafsFlags["point"]);
37214
- case "polygon":
37215
- if (graph.numSides === UNLIMITED) {
37216
- return Boolean(mafsFlags["unlimited-polygon"]);
37217
- }
37218
- return Boolean(mafsFlags["polygon"]);
37219
- default:
37220
- return Boolean(mafsFlags[graph.type]);
37221
- }
37222
- }
37223
35886
  // We don't need to change any of the original props for static mode
37224
35887
  const staticTransform$3 = ___default["default"].identity;
37225
35888
  var InteractiveGraph$1 = {
@@ -37227,9 +35890,7 @@ var InteractiveGraph$1 = {
37227
35890
  displayName: "Interactive graph (Assessments only)",
37228
35891
  widget: InteractiveGraph,
37229
35892
  staticTransform: staticTransform$3,
37230
- // TODO(LEMS-2656): remove TS suppression
37231
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
37232
- scorer: perseusScore.scoreInteractiveGraph
35893
+ getPublicWidgetOptions: perseusCore.getInteractiveGraphPublicWidgetOptions
37233
35894
  };
37234
35895
 
37235
35896
  // TODO: can we use WB typography?
@@ -38187,9 +36848,7 @@ var LabelImage$1 = {
38187
36848
  widget: LabelImageWithDependencies,
38188
36849
  accessible: true,
38189
36850
  isLintable: true,
38190
- // TODO(LEMS-2656): remove TS suppression
38191
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
38192
- scorer: perseusScore.scoreLabelImage
36851
+ getPublicWidgetOptions: perseusCore.getLabelImagePublicWidgetOptions
38193
36852
  };
38194
36853
 
38195
36854
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -38426,7 +37085,7 @@ class Draggable extends React__namespace.Component {
38426
37085
  // We need to keep backwards compatbility with rules specified directly
38427
37086
  // in CSS. Hence the hacky tacking on of manual classNames.
38428
37087
  // See sortable.less for details.
38429
- let className = aphrodite.css(styles$8.card, styles$8.draggable, layout === "horizontal" && styles$8.horizontalCard, layout === "vertical" && styles$8.verticalCard, type === ItemState.DRAGGING && styles$8.dragging, type === ItemState.DISABLED && styles$8.disabled, !includePadding && styles$8.unpaddedCard) + " " + ClassNames.INTERACTIVE + " perseus-sortable-draggable";
37088
+ let className = aphrodite.css(styles$8.card, styles$8.draggable, layout === "horizontal" && styles$8.horizontalCard, layout === "vertical" && styles$8.verticalCard, type === ItemState.DRAGGING && styles$8.dragging, type === ItemState.DISABLED && styles$8.disabled, !includePadding && styles$8.unpaddedCard) + " " + "perseus-sortable-draggable";
38430
37089
  if (!includePadding) {
38431
37090
  className += " perseus-sortable-draggable-unpadded";
38432
37091
  }
@@ -39117,10 +37776,7 @@ var Matcher$1 = {
39117
37776
  name: "matcher",
39118
37777
  displayName: "Matcher (two column)",
39119
37778
  widget: Matcher,
39120
- isLintable: true,
39121
- // TODO(LEMS-2656): remove TS suppression
39122
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
39123
- scorer: perseusScore.scoreMatcher
37779
+ isLintable: true
39124
37780
  };
39125
37781
 
39126
37782
  const getPromptJSON$8 = (renderProps, userInput) => {
@@ -39524,12 +38180,7 @@ var Matrix$1 = {
39524
38180
  transform: propTransform$1,
39525
38181
  staticTransform: staticTransform$2,
39526
38182
  isLintable: true,
39527
- // TODO(LEMS-2656): remove TS suppression
39528
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39529
- scorer: perseusScore.scoreMatrix,
39530
- // TODO(LEMS-2656): remove TS suppression
39531
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39532
- validator: perseusScore.validateMatrix
38183
+ getPublicWidgetOptions: perseusCore.getMatrixPublicWidgetOptions
39533
38184
  };
39534
38185
 
39535
38186
  const getPromptJSON$7 = () => {
@@ -39619,7 +38270,7 @@ class Measurer extends React__namespace.Component {
39619
38270
  // TODO(scottgrant): This isn't a11y-friendly! We should insist on
39620
38271
  // finding some valid alt text when this widget is used.
39621
38272
  return /*#__PURE__*/React__namespace.createElement("div", {
39622
- className: "perseus-widget perseus-widget-measurer " + "graphie-container above-scratchpad",
38273
+ className: "perseus-widget perseus-widget-measurer " + "graphie-container blank-background",
39623
38274
  style: {
39624
38275
  width: this.props.box[0],
39625
38276
  height: this.props.box[1]
@@ -39644,9 +38295,7 @@ var Measurer$1 = {
39644
38295
  hidden: true,
39645
38296
  widget: Measurer,
39646
38297
  version: perseusCore.measurerLogic.version,
39647
- propUpgrades: perseusCore.measurerLogic.widgetOptionsUpgrades,
39648
- // TODO: things that aren't interactive shouldn't need scoring functions
39649
- scorer: () => scoreNoop(1)
38298
+ propUpgrades: perseusCore.measurerLogic.widgetOptionsUpgrades
39650
38299
  };
39651
38300
 
39652
38301
  /**
@@ -40534,8 +39183,7 @@ var Molecule$1 = {
40534
39183
  name: "molecule-renderer",
40535
39184
  displayName: "Molecule renderer",
40536
39185
  hidden: true,
40537
- widget: MoleculeWidget,
40538
- scorer: () => scoreNoop()
39186
+ widget: MoleculeWidget
40539
39187
  };
40540
39188
 
40541
39189
  const getPromptJSON$6 = (renderProps, userInput) => {
@@ -41110,12 +39758,7 @@ var NumberLine$1 = {
41110
39758
  widget: NumberLine,
41111
39759
  transform: numberLineTransform,
41112
39760
  staticTransform: staticTransform$1,
41113
- // TODO(LEMS-2656): remove TS suppression
41114
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
41115
- scorer: perseusScore.scoreNumberLine,
41116
- // TODO(LEMS-2656): remove TS suppression
41117
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
41118
- validator: perseusScore.validateNumberLine
39761
+ getPublicWidgetOptions: perseusCore.getNumberLinePublicWidgetOptions
41119
39762
  };
41120
39763
 
41121
39764
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -41133,7 +39776,7 @@ const getPromptJSON$5 = (renderProps, userInput) => {
41133
39776
  class PlaceholderCard extends React__namespace.Component {
41134
39777
  render() {
41135
39778
  return /*#__PURE__*/React__namespace.createElement("div", {
41136
- className: "card-wrap " + ClassNames.INTERACTIVE,
39779
+ className: "card-wrap",
41137
39780
  style: {
41138
39781
  width: this.props.width
41139
39782
  }
@@ -41148,7 +39791,7 @@ class PlaceholderCard extends React__namespace.Component {
41148
39791
  class DragHintCard extends React__namespace.Component {
41149
39792
  render() {
41150
39793
  return /*#__PURE__*/React__namespace.createElement("div", {
41151
- className: "card-wrap " + ClassNames.INTERACTIVE
39794
+ className: "card-wrap"
41152
39795
  }, /*#__PURE__*/React__namespace.createElement("div", {
41153
39796
  className: "card drag-hint"
41154
39797
  }));
@@ -41283,7 +39926,7 @@ class Card extends React__namespace.Component {
41283
39926
  const rendererProps = ___default["default"].pick(this.props, "content");
41284
39927
  const onMouseDown = this.props.animating ? $__default["default"].noop : this.onMouseDown;
41285
39928
  return /*#__PURE__*/React__namespace.createElement("div", {
41286
- className: "card-wrap " + ClassNames.INTERACTIVE,
39929
+ className: "card-wrap",
41287
39930
  style: style,
41288
39931
  onMouseDown: onMouseDown,
41289
39932
  onTouchStart: onMouseDown,
@@ -41654,7 +40297,7 @@ class Orderer extends React__namespace.Component {
41654
40297
  });
41655
40298
  }, this));
41656
40299
  return /*#__PURE__*/React__namespace.createElement("div", {
41657
- className: "draggy-boxy-thing orderer " + "height-" + this.props.height + " " + "layout-" + this.props.layout + " " + "above-scratchpad blank-background " + "perseus-clearfix " + ClassNames.INTERACTIVE
40300
+ className: "draggy-boxy-thing orderer " + "height-" + this.props.height + " " + "layout-" + this.props.layout + " " + "blank-background " + "perseus-clearfix "
41658
40301
  // eslint-disable-next-line react/no-string-refs
41659
40302
  ,
41660
40303
  ref: "orderer"
@@ -41667,12 +40310,6 @@ var Orderer$1 = {
41667
40310
  hidden: true,
41668
40311
  widget: Orderer,
41669
40312
  isLintable: true,
41670
- // TODO(LEMS-2656): remove TS suppression
41671
- // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41672
- scorer: perseusScore.scoreOrderer,
41673
- // TODO(LEMS-2656): remove TS suppression
41674
- // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41675
- validator: perseusScore.validateOrderer,
41676
40313
  getPublicWidgetOptions: perseusCore.getOrdererPublicWidgetOptions
41677
40314
  };
41678
40315
 
@@ -41716,15 +40353,12 @@ class PassageRefTarget extends React__namespace.Component {
41716
40353
  var PassageRefTarget$1 = {
41717
40354
  name: "passage-ref-target",
41718
40355
  displayName: "PassageRefTarget",
41719
- defaultAlignment: "inline",
41720
40356
  widget: PassageRefTarget,
41721
40357
  hidden: true,
41722
40358
  transform: editorProps => {
41723
40359
  return ___default["default"].pick(editorProps, "content");
41724
40360
  },
41725
- isLintable: true,
41726
- // TODO: things that aren't interactive shouldn't need scoring functions
41727
- scorer: () => scoreNoop()
40361
+ isLintable: true
41728
40362
  };
41729
40363
 
41730
40364
  const getPromptJSON$4 = () => {
@@ -42726,7 +41360,7 @@ class Plotter extends React__namespace.Component {
42726
41360
  marginBottom: this.props.labels[0] ? paddingForBottomLabel : 0
42727
41361
  };
42728
41362
  return /*#__PURE__*/React__namespace.createElement("div", {
42729
- className: "perseus-widget-plotter graphie " + ClassNames.INTERACTIVE
41363
+ className: "perseus-widget-plotter graphie"
42730
41364
  // eslint-disable-next-line react/no-string-refs
42731
41365
  ,
42732
41366
  ref: "graphieDiv",
@@ -42743,12 +41377,7 @@ var Plotter$1 = {
42743
41377
  hidden: true,
42744
41378
  widget: Plotter,
42745
41379
  staticTransform: staticTransform,
42746
- // TODO(LEMS-2656): remove TS suppression
42747
- // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42748
- scorer: perseusScore.scorePlotter,
42749
- // TODO(LEMS-2656): remove TS suppression
42750
- // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42751
- validator: perseusScore.validatePlotter
41380
+ getPublicWidgetOptions: perseusCore.getPlotterPublicWidgetOptions
42752
41381
  };
42753
41382
 
42754
41383
  const getPromptJSON$2 = () => {
@@ -42909,12 +41538,7 @@ var Sorter$1 = {
42909
41538
  displayName: "Sorter",
42910
41539
  widget: Sorter,
42911
41540
  isLintable: true,
42912
- // TODO(LEMS-2656): remove TS suppression
42913
- // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42914
- scorer: perseusScore.scoreSorter,
42915
- // TODO(LEMS-2656): remove TS suppression
42916
- // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42917
- validator: perseusScore.validateSorter
41541
+ getPublicWidgetOptions: perseusCore.getSorterPublicWidgetOptions
42918
41542
  };
42919
41543
 
42920
41544
  const {
@@ -43131,12 +41755,7 @@ var Table$1 = {
43131
41755
  transform: propTransform,
43132
41756
  hidden: true,
43133
41757
  isLintable: true,
43134
- // TODO(LEMS-2656): remove TS suppression
43135
- // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
43136
- scorer: perseusScore.scoreTable,
43137
- // TODO(LEMS-2656): remove TS suppression
43138
- // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
43139
- validator: perseusScore.validateTable
41758
+ getPublicWidgetOptions: perseusCore.getTablePublicWidgetOptions
43140
41759
  };
43141
41760
 
43142
41761
  const getPromptJSON = () => {
@@ -43298,11 +41917,7 @@ class Video extends React__namespace.Component {
43298
41917
  var Video$1 = {
43299
41918
  name: "video",
43300
41919
  displayName: "Video",
43301
- defaultAlignment: "block",
43302
- supportedAlignments: ["block", "float-left", "float-right", "full-width"],
43303
- widget: Video,
43304
- // TODO: things that aren't interactive shouldn't need scoring functions
43305
- scorer: () => scoreNoop()
41920
+ widget: Video
43306
41921
  };
43307
41922
 
43308
41923
  /* eslint-disable import/no-named-as-default */
@@ -43321,7 +41936,7 @@ const init = function () {
43321
41936
 
43322
41937
  // This file is processed by a Rollup plugin (replace) to inject the production
43323
41938
  const libName = "@khanacademy/perseus";
43324
- const libVersion = "52.0.1";
41939
+ const libVersion = "53.1.0";
43325
41940
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
43326
41941
 
43327
41942
  const apiVersion = {
@@ -44197,6 +42812,42 @@ const ref = /*#__PURE__*/React__namespace.forwardRef((props, ref) => /*#__PURE__
44197
42812
  }));
44198
42813
  }));
44199
42814
 
42815
+ function isCorrect(score) {
42816
+ return score.type === "points" && score.earned >= score.total;
42817
+ }
42818
+ function keScoreFromPerseusScore(score,
42819
+ // It's weird, but this is what we're passing it
42820
+ guess, state) {
42821
+ if (score.type === "points") {
42822
+ return {
42823
+ empty: false,
42824
+ correct: isCorrect(score),
42825
+ message: score.message,
42826
+ guess: guess,
42827
+ state: state
42828
+ };
42829
+ }
42830
+ if (score.type === "invalid") {
42831
+ return {
42832
+ empty: true,
42833
+ correct: false,
42834
+ message: score.message,
42835
+ suppressAlmostThere: score.suppressAlmostThere,
42836
+ guess: guess,
42837
+ state: state
42838
+ };
42839
+ }
42840
+ throw new perseusCore.PerseusError(
42841
+ // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
42842
+ "Invalid score type: " + score.type, perseusCore.Errors.InvalidInput, {
42843
+ metadata: {
42844
+ score: JSON.stringify(score),
42845
+ guess: JSON.stringify(guess),
42846
+ state: JSON.stringify(state)
42847
+ }
42848
+ });
42849
+ }
42850
+
44200
42851
  /**
44201
42852
  * Traverses a {content, widgets, images} renderer props object,
44202
42853
  * such as `itemData.question`
@@ -44217,7 +42868,7 @@ const deepCallbackFor = function (contentCallback, widgetCallback, optionsCallba
44217
42868
  // This doesn't modify the widget info if the widget info
44218
42869
  // is at a later version than is supported, which is important
44219
42870
  // for our latestVersion test below.
44220
- const upgradedWidgetInfo = upgradeWidgetInfoToLatestVersion(widgetInfo);
42871
+ const upgradedWidgetInfo = perseusCore.upgradeWidgetInfoToLatestVersion(widgetInfo);
44221
42872
  const latestVersion = getVersion(upgradedWidgetInfo.type);
44222
42873
 
44223
42874
  // Only traverse our children if we can understand this version
@@ -44329,7 +42980,7 @@ const isRawWidgetInfoRenderableBy = function (widgetInfo, rendererContentVersion
44329
42980
 
44330
42981
  // NOTE: This doesn't modify the widget info if the widget info
44331
42982
  // is at a later version than is supported.
44332
- const upgradedWidgetInfo = upgradeWidgetInfoToLatestVersion(widgetInfo);
42983
+ const upgradedWidgetInfo = perseusCore.upgradeWidgetInfoToLatestVersion(widgetInfo);
44333
42984
  return isUpgradedWidgetInfoRenderableBy(upgradedWidgetInfo, rendererContentVersion[upgradedWidgetInfo.type]);
44334
42985
  };
44335
42986
  const isRendererContentRenderableBy = function (rendererOptions, rendererContentVersion) {
@@ -45677,7 +44328,6 @@ exports.mediaQueries = mediaQueries;
45677
44328
  exports.parseDataFromJSONP = parseDataFromJSONP;
45678
44329
  exports.preprocessTex = preprocessTex;
45679
44330
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
45680
- exports.scorePerseusItem = scorePerseusItem;
45681
44331
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
45682
44332
  exports.traverse = traverse;
45683
44333
  exports.usePerseusI18n = usePerseusI18n;