@khanacademy/perseus 23.5.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/es/index.js CHANGED
@@ -520,8 +520,7 @@ class InnerMathInput extends React.Component {
520
520
  active: false
521
521
  }) : /*#__PURE__*/React.createElement(Clickable, {
522
522
  "aria-label": this.state.keypadOpen ? this.context.strings.closeKeypad : this.context.strings.openKeypad,
523
- "aria-checked": this.state.keypadOpen,
524
- role: "switch",
523
+ role: "button",
525
524
  onClick: () => this.state.keypadOpen ? this.closeKeypad() : this.openKeypad()
526
525
  }, props => /*#__PURE__*/React.createElement(MathInputIcon, _extends({
527
526
  active: this.state.keypadOpen
@@ -20964,254 +20963,6 @@ const styles$B = {
20964
20963
  }
20965
20964
  };
20966
20965
 
20967
- /*
20968
- * In this file, an `expression` is some portion of valid TeX enclosed in
20969
- * curly brackets.
20970
- */
20971
-
20972
- /*
20973
- * Find the index at which an expression ends, i.e., has an unmatched
20974
- * closing curly bracket. This method assumes that we start with a non-open
20975
- * bracket character and end when we've seen more left than right brackets
20976
- * (rather than assuming that we start with a bracket character and wait for
20977
- * bracket equality).
20978
- */
20979
- function findEndpoint(tex, currentIndex) {
20980
- let bracketDepth = 0;
20981
- for (let i = currentIndex, len = tex.length; i < len; i++) {
20982
- const c = tex[i];
20983
- if (c === "{") {
20984
- bracketDepth++;
20985
- } else if (c === "}") {
20986
- bracketDepth--;
20987
- }
20988
- if (bracketDepth < 0) {
20989
- return i;
20990
- }
20991
- }
20992
- // If we never see unbalanced curly brackets, default to the
20993
- // entire string
20994
- return tex.length;
20995
- }
20996
-
20997
- /*
20998
- * Parses an individual set of curly brackets into TeX.
20999
- */
21000
- function parseNextExpression(tex, currentIndex, handler) {
21001
- // Find the first '{' and grab subsequent TeX
21002
- // Ex) tex: '{3}{7}', and we want the '3'
21003
- const openBracketIndex = tex.indexOf("{", currentIndex);
21004
- const nextExpIndex = openBracketIndex + 1;
21005
-
21006
- // Truncate to only contain remaining TeX
21007
- const endpoint = findEndpoint(tex, nextExpIndex);
21008
- const expressionTeX = tex.substring(nextExpIndex, endpoint);
21009
- const parsedExp = walkTex(expressionTeX, handler);
21010
- return {
21011
- endpoint: endpoint,
21012
- expression: parsedExp
21013
- };
21014
- }
21015
- function getNextFracIndex(tex, currentIndex) {
21016
- const dfrac = "\\dfrac";
21017
- const frac = "\\frac";
21018
- const nextFrac = tex.indexOf(frac, currentIndex);
21019
- const nextDFrac = tex.indexOf(dfrac, currentIndex);
21020
- if (nextFrac > -1 && nextDFrac > -1) {
21021
- return Math.min(nextFrac, nextDFrac);
21022
- }
21023
- if (nextFrac > -1) {
21024
- return nextFrac;
21025
- }
21026
- if (nextDFrac > -1) {
21027
- return nextDFrac;
21028
- }
21029
- return -1;
21030
- }
21031
- function walkTex(tex, handler) {
21032
- if (!tex) {
21033
- return "";
21034
- }
21035
-
21036
- // Ex) tex: '2 \dfrac {3}{7}'
21037
- let parsedString = "";
21038
- let currentIndex = 0;
21039
- let nextFrac = getNextFracIndex(tex, currentIndex);
21040
-
21041
- // For each \dfrac, find the two expressions (wrapped in {}) and recur
21042
- while (nextFrac > -1) {
21043
- // Gather first fragment, preceding \dfrac
21044
- // Ex) parsedString: '2 '
21045
- parsedString += tex.substring(currentIndex, nextFrac);
21046
-
21047
- // Remove everything preceding \dfrac, which has been parsed
21048
- currentIndex = nextFrac;
21049
-
21050
- // Parse first expression and move index past it
21051
- // Ex) firstParsedExpression.expression: '3'
21052
- const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
21053
- currentIndex = firstParsedExpression.endpoint + 1;
21054
-
21055
- // Parse second expression
21056
- // Ex) secondParsedExpression.expression: '7'
21057
- const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
21058
- currentIndex = secondParsedExpression.endpoint + 1;
21059
-
21060
- // Add expressions to running total of parsed expressions
21061
- if (parsedString.length) {
21062
- parsedString += " ";
21063
- }
21064
-
21065
- // Apply a custom handler based on the parsed subexpressions
21066
- parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21067
-
21068
- // Find next DFrac, relative to currentIndex
21069
- nextFrac = getNextFracIndex(tex, currentIndex);
21070
- }
21071
-
21072
- // Add remaining TeX, which is \dfrac-free
21073
- parsedString += tex.slice(currentIndex);
21074
- return parsedString;
21075
- }
21076
-
21077
- /*
21078
- * Modify a TeX expression, returning another TeX expression. The resulting
21079
- * expression will have its innermost fractions stubbed out with \fracs
21080
- * (as opposed to \dfracs). All other content will remain untouched.
21081
- */
21082
- function modifyTex(tex) {
21083
- function isNestedFraction(tex) {
21084
- return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21085
- }
21086
- const handler = function handler(exp1, exp2) {
21087
- let prefix;
21088
- if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21089
- prefix = "\\dfrac";
21090
- } else {
21091
- prefix = "\\frac";
21092
- }
21093
- return prefix + " {" + exp1 + "}{" + exp2 + "}";
21094
- };
21095
- return walkTex(tex, handler);
21096
- }
21097
-
21098
- /*
21099
- * Parse a TeX expression into something interpretable by input-number.
21100
- * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21101
- * (2) removing backslash-escaping from certain characters (right now, only
21102
- * percent signs).
21103
- *
21104
- * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21105
- * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21106
- * recursion is to allow for nested \dfrac elements.
21107
- *
21108
- * Backslash-escapes are removed with a simple search-and-replace.
21109
- */
21110
- function parseTex(tex) {
21111
- const handler = function handler(exp1, exp2) {
21112
- return exp1 + "/" + exp2;
21113
- };
21114
- const texWithoutFracs = walkTex(tex, handler);
21115
- return texWithoutFracs.replace("\\%", "%");
21116
- }
21117
- var TexWrangler = {
21118
- parseTex,
21119
- modifyTex
21120
- };
21121
-
21122
- /* eslint-disable react/sort-comp */
21123
- const ModifyTex = TexWrangler.modifyTex;
21124
- class MathOutput extends React.Component {
21125
- constructor(...args) {
21126
- super(...args);
21127
- this.state = {
21128
- focused: false,
21129
- selectorNamespace: _.uniqueId("math-output")
21130
- };
21131
- this._getInputClassName = () => {
21132
- let className = "math-output " + ClassNames.INPUT + " " + ClassNames.INTERACTIVE;
21133
- if (this.state.focused) {
21134
- className += " " + ClassNames.FOCUSED;
21135
- }
21136
- if (this.props.className) {
21137
- className += " " + this.props.className;
21138
- }
21139
- return className;
21140
- };
21141
- this._getDisplayValue = value => {
21142
- // Cast from (potentially a) number to string
21143
- let displayText;
21144
- if (value != null) {
21145
- displayText = "" + value;
21146
- } else {
21147
- displayText = "";
21148
- }
21149
- return ModifyTex(displayText);
21150
- };
21151
- this.getValue = () => {
21152
- return this.props.value;
21153
- };
21154
- this.focus = () => {
21155
- if (!this.state.focused) {
21156
- this.props.onFocus();
21157
- this._bindBlurHandler();
21158
- this.setState({
21159
- focused: true
21160
- });
21161
- }
21162
- };
21163
- this.blur = () => {
21164
- if (this.state.focused) {
21165
- this.props.onBlur();
21166
- this._unbindBlurHandler();
21167
- this.setState({
21168
- focused: false
21169
- });
21170
- }
21171
- };
21172
- this._bindBlurHandler = () => {
21173
- $(document).bind("vclick." + this.state.selectorNamespace, e => {
21174
- // Detect whether the target has our React DOM node as a parent
21175
- const $closestWidget = $(e.target).closest(
21176
- // @ts-expect-error - TS2769 - No overload matches this call.
21177
- ReactDOM__default.findDOMNode(this));
21178
- if (!$closestWidget.length) {
21179
- this.blur();
21180
- }
21181
- });
21182
- };
21183
- this._unbindBlurHandler = () => {
21184
- $(document).unbind("." + this.state.selectorNamespace);
21185
- };
21186
- }
21187
- render() {
21188
- const divStyle = {
21189
- textAlign: "center"
21190
- };
21191
- const {
21192
- TeX
21193
- } = getDependencies();
21194
- return /*#__PURE__*/React.createElement("span", {
21195
- // eslint-disable-next-line react/no-string-refs
21196
- ref: "input",
21197
- className: this._getInputClassName(),
21198
- "aria-label": this.props.labelText,
21199
- onMouseDown: this.focus,
21200
- onTouchStart: this.focus
21201
- }, /*#__PURE__*/React.createElement("div", {
21202
- style: divStyle
21203
- }, /*#__PURE__*/React.createElement(TeX, null, this._getDisplayValue(this.props.value))));
21204
- }
21205
- componentWillUnmount() {
21206
- this._unbindBlurHandler();
21207
- }
21208
- }
21209
- MathOutput.defaultProps = {
21210
- value: "",
21211
- onFocus: function () {},
21212
- onBlur: function () {}
21213
- };
21214
-
21215
20966
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
21216
20967
  let lastId = 0;
21217
20968
  function uniqueIdForInput(prefix = "input-") {
@@ -21566,12 +21317,6 @@ class InputWithExamples extends React.Component {
21566
21317
  return `input-with-examples-${btoa(this.props.id).replace(/=/g, "")}`;
21567
21318
  };
21568
21319
  this._getInputClassName = () => {
21569
- // <MathOutput> is a special component that manages its own class and
21570
- // state, as it's a <span> that wants to act like an <input>.
21571
- if (this.props.type === "tex") {
21572
- return this.props.className;
21573
- }
21574
-
21575
21320
  // Otherwise, we need to add these INPUT and FOCUSED tags here.
21576
21321
  let className = ClassNames.INPUT + " " + ClassNames.INTERACTIVE;
21577
21322
  if (this.state.focused) {
@@ -21582,8 +21327,7 @@ class InputWithExamples extends React.Component {
21582
21327
  }
21583
21328
  return className;
21584
21329
  };
21585
- this._getPropsForInputType = () => {
21586
- // Minimal set of props, used by each input type
21330
+ this._renderInput = () => {
21587
21331
  const id = this._getUniqueId();
21588
21332
  const inputProps = {
21589
21333
  id: id,
@@ -21595,52 +21339,15 @@ class InputWithExamples extends React.Component {
21595
21339
  onFocus: this._handleFocus,
21596
21340
  onBlur: this._handleBlur,
21597
21341
  disabled: this.props.disabled,
21598
- style: this.props.style
21599
- };
21600
- if (this.props.type === "tex") {
21601
- return inputProps;
21602
- }
21603
-
21604
- // Add useful props required for MATH and TEXT modes
21605
- _.extend(inputProps, {
21342
+ style: this.props.style,
21606
21343
  onChange: this.props.onChange,
21607
- onTouchStart: captureScratchpadTouchStart$2
21608
- });
21609
-
21610
- // And add final props that are MATH- and TEXT-specific
21611
- if (this.props.type === "math") {
21612
- return _.extend({
21613
- buttonSet: this.props.buttonSet,
21614
- buttonsVisible: this.props.buttonsVisible,
21615
- convertDotToTimes: this.props.convertDotToTimes
21616
- }, inputProps);
21617
- }
21618
- if (this.props.type === "text") {
21619
- return _.extend({
21620
- autoCapitalize: "off",
21621
- autoComplete: "off",
21622
- autoCorrect: "off",
21623
- spellCheck: "false"
21624
- }, inputProps);
21625
- }
21626
- };
21627
- this._getComponentForInputType = () => {
21628
- switch (this.props.type) {
21629
- case "tex":
21630
- return MathOutput;
21631
- case "math":
21632
- return MathInput;
21633
- case "text":
21634
- return TextInput;
21635
- default:
21636
- this.props.type;
21637
- return null;
21638
- }
21639
- };
21640
- this._renderInput = () => {
21641
- const inputProps = this._getPropsForInputType();
21642
- const InputComponent = this._getComponentForInputType();
21643
- return /*#__PURE__*/React.createElement(InputComponent, inputProps);
21344
+ onTouchStart: captureScratchpadTouchStart$2,
21345
+ autoCapitalize: "off",
21346
+ autoComplete: "off",
21347
+ autoCorrect: "off",
21348
+ spellCheck: "false"
21349
+ };
21350
+ return /*#__PURE__*/React.createElement(TextInput, inputProps);
21644
21351
  };
21645
21352
  this._handleFocus = () => {
21646
21353
  this.props.onFocus();
@@ -21682,14 +21389,6 @@ class InputWithExamples extends React.Component {
21682
21389
  }
21683
21390
  render() {
21684
21391
  const input = this._renderInput();
21685
-
21686
- // Static rendering, which doesn't include the 'tooltip' logic that the
21687
- // other types require, and is hence handled separately.
21688
- if (this.props.type === "tex") {
21689
- return input;
21690
- }
21691
-
21692
- // Else, we need to be able to show examples
21693
21392
  const examplesContent = _.map(this.props.examples, example => {
21694
21393
  return "- " + example;
21695
21394
  }).join("\n");
@@ -21716,7 +21415,6 @@ class InputWithExamples extends React.Component {
21716
21415
  }
21717
21416
  InputWithExamples.contextType = PerseusI18nContext;
21718
21417
  InputWithExamples.defaultProps = {
21719
- type: "text",
21720
21418
  shouldShowExamples: true,
21721
21419
  onFocus: function () {},
21722
21420
  onBlur: function () {},
@@ -21805,6 +21503,161 @@ SimpleKeypadInput.propTypes = {
21805
21503
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
21806
21504
  };
21807
21505
 
21506
+ /*
21507
+ * In this file, an `expression` is some portion of valid TeX enclosed in
21508
+ * curly brackets.
21509
+ */
21510
+
21511
+ /*
21512
+ * Find the index at which an expression ends, i.e., has an unmatched
21513
+ * closing curly bracket. This method assumes that we start with a non-open
21514
+ * bracket character and end when we've seen more left than right brackets
21515
+ * (rather than assuming that we start with a bracket character and wait for
21516
+ * bracket equality).
21517
+ */
21518
+ function findEndpoint(tex, currentIndex) {
21519
+ let bracketDepth = 0;
21520
+ for (let i = currentIndex, len = tex.length; i < len; i++) {
21521
+ const c = tex[i];
21522
+ if (c === "{") {
21523
+ bracketDepth++;
21524
+ } else if (c === "}") {
21525
+ bracketDepth--;
21526
+ }
21527
+ if (bracketDepth < 0) {
21528
+ return i;
21529
+ }
21530
+ }
21531
+ // If we never see unbalanced curly brackets, default to the
21532
+ // entire string
21533
+ return tex.length;
21534
+ }
21535
+
21536
+ /*
21537
+ * Parses an individual set of curly brackets into TeX.
21538
+ */
21539
+ function parseNextExpression(tex, currentIndex, handler) {
21540
+ // Find the first '{' and grab subsequent TeX
21541
+ // Ex) tex: '{3}{7}', and we want the '3'
21542
+ const openBracketIndex = tex.indexOf("{", currentIndex);
21543
+ const nextExpIndex = openBracketIndex + 1;
21544
+
21545
+ // Truncate to only contain remaining TeX
21546
+ const endpoint = findEndpoint(tex, nextExpIndex);
21547
+ const expressionTeX = tex.substring(nextExpIndex, endpoint);
21548
+ const parsedExp = walkTex(expressionTeX, handler);
21549
+ return {
21550
+ endpoint: endpoint,
21551
+ expression: parsedExp
21552
+ };
21553
+ }
21554
+ function getNextFracIndex(tex, currentIndex) {
21555
+ const dfrac = "\\dfrac";
21556
+ const frac = "\\frac";
21557
+ const nextFrac = tex.indexOf(frac, currentIndex);
21558
+ const nextDFrac = tex.indexOf(dfrac, currentIndex);
21559
+ if (nextFrac > -1 && nextDFrac > -1) {
21560
+ return Math.min(nextFrac, nextDFrac);
21561
+ }
21562
+ if (nextFrac > -1) {
21563
+ return nextFrac;
21564
+ }
21565
+ if (nextDFrac > -1) {
21566
+ return nextDFrac;
21567
+ }
21568
+ return -1;
21569
+ }
21570
+ function walkTex(tex, handler) {
21571
+ if (!tex) {
21572
+ return "";
21573
+ }
21574
+
21575
+ // Ex) tex: '2 \dfrac {3}{7}'
21576
+ let parsedString = "";
21577
+ let currentIndex = 0;
21578
+ let nextFrac = getNextFracIndex(tex, currentIndex);
21579
+
21580
+ // For each \dfrac, find the two expressions (wrapped in {}) and recur
21581
+ while (nextFrac > -1) {
21582
+ // Gather first fragment, preceding \dfrac
21583
+ // Ex) parsedString: '2 '
21584
+ parsedString += tex.substring(currentIndex, nextFrac);
21585
+
21586
+ // Remove everything preceding \dfrac, which has been parsed
21587
+ currentIndex = nextFrac;
21588
+
21589
+ // Parse first expression and move index past it
21590
+ // Ex) firstParsedExpression.expression: '3'
21591
+ const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
21592
+ currentIndex = firstParsedExpression.endpoint + 1;
21593
+
21594
+ // Parse second expression
21595
+ // Ex) secondParsedExpression.expression: '7'
21596
+ const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
21597
+ currentIndex = secondParsedExpression.endpoint + 1;
21598
+
21599
+ // Add expressions to running total of parsed expressions
21600
+ if (parsedString.length) {
21601
+ parsedString += " ";
21602
+ }
21603
+
21604
+ // Apply a custom handler based on the parsed subexpressions
21605
+ parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21606
+
21607
+ // Find next DFrac, relative to currentIndex
21608
+ nextFrac = getNextFracIndex(tex, currentIndex);
21609
+ }
21610
+
21611
+ // Add remaining TeX, which is \dfrac-free
21612
+ parsedString += tex.slice(currentIndex);
21613
+ return parsedString;
21614
+ }
21615
+
21616
+ /*
21617
+ * Modify a TeX expression, returning another TeX expression. The resulting
21618
+ * expression will have its innermost fractions stubbed out with \fracs
21619
+ * (as opposed to \dfracs). All other content will remain untouched.
21620
+ */
21621
+ function modifyTex(tex) {
21622
+ function isNestedFraction(tex) {
21623
+ return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21624
+ }
21625
+ const handler = function handler(exp1, exp2) {
21626
+ let prefix;
21627
+ if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21628
+ prefix = "\\dfrac";
21629
+ } else {
21630
+ prefix = "\\frac";
21631
+ }
21632
+ return prefix + " {" + exp1 + "}{" + exp2 + "}";
21633
+ };
21634
+ return walkTex(tex, handler);
21635
+ }
21636
+
21637
+ /*
21638
+ * Parse a TeX expression into something interpretable by input-number.
21639
+ * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21640
+ * (2) removing backslash-escaping from certain characters (right now, only
21641
+ * percent signs).
21642
+ *
21643
+ * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21644
+ * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21645
+ * recursion is to allow for nested \dfrac elements.
21646
+ *
21647
+ * Backslash-escapes are removed with a simple search-and-replace.
21648
+ */
21649
+ function parseTex(tex) {
21650
+ const handler = function handler(exp1, exp2) {
21651
+ return exp1 + "/" + exp2;
21652
+ };
21653
+ const texWithoutFracs = walkTex(tex, handler);
21654
+ return texWithoutFracs.replace("\\%", "%");
21655
+ }
21656
+ var TexWrangler = {
21657
+ parseTex,
21658
+ modifyTex
21659
+ };
21660
+
21808
21661
  /* eslint-disable react/sort-comp */
21809
21662
  const ParseTex$1 = TexWrangler.parseTex;
21810
21663
  const answerTypes = {
@@ -21983,7 +21836,6 @@ class InputNumber extends React.Component {
21983
21836
  value: this.props.currentValue,
21984
21837
  onChange: this.handleChange,
21985
21838
  style: inputStyles,
21986
- type: "text",
21987
21839
  examples: this.examples(),
21988
21840
  shouldShowExamples: this.shouldShowExamples(),
21989
21841
  onFocus: this._handleFocus,
@@ -22392,7 +22244,6 @@ class NumericInput extends React.Component {
22392
22244
  value: this.props.currentValue,
22393
22245
  onChange: this.handleChange,
22394
22246
  labelText: labelText,
22395
- type: "text",
22396
22247
  examples: this.examples(),
22397
22248
  shouldShowExamples: this.shouldShowExamples(),
22398
22249
  onFocus: this._handleFocus,
@@ -29120,6 +28971,10 @@ function snap(snapStep, point) {
29120
28971
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29121
28972
  }
29122
28973
 
28974
+ function useDraggable(params) {
28975
+ return useMovable(params);
28976
+ }
28977
+
29123
28978
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
29124
28979
  // graph.
29125
28980
  const hitboxSizePx = 48;
@@ -29273,7 +29128,7 @@ function useControlPoint(point, color, onMovePoint) {
29273
29128
  } = useGraphConfig();
29274
29129
  const [focused, setFocused] = useState(false);
29275
29130
  const keyboardHandleRef = useRef(null);
29276
- useMovable({
29131
+ useDraggable({
29277
29132
  gestureTarget: keyboardHandleRef,
29278
29133
  point,
29279
29134
  onMove: onMovePoint,
@@ -29282,7 +29137,7 @@ function useControlPoint(point, color, onMovePoint) {
29282
29137
  const visiblePointRef = useRef(null);
29283
29138
  const {
29284
29139
  dragging
29285
- } = useMovable({
29140
+ } = useDraggable({
29286
29141
  gestureTarget: visiblePointRef,
29287
29142
  point,
29288
29143
  onMove: onMovePoint,
@@ -29336,7 +29191,7 @@ const Line$1 = props => {
29336
29191
  const line = useRef(null);
29337
29192
  const {
29338
29193
  dragging
29339
- } = useMovable({
29194
+ } = useDraggable({
29340
29195
  gestureTarget: line,
29341
29196
  point: start,
29342
29197
  onMove: newPoint => {
@@ -29676,7 +29531,7 @@ const StyledMovablePoint = props => {
29676
29531
  const snapToValue = snapTo != null ? snapTo : "grid";
29677
29532
  const {
29678
29533
  dragging
29679
- } = useMovable({
29534
+ } = useDraggable({
29680
29535
  gestureTarget: elementRef,
29681
29536
  point,
29682
29537
  onMove,
@@ -29718,7 +29573,7 @@ const PolygonGraph = props => {
29718
29573
  const snapToValue = snapTo != null ? snapTo : "grid";
29719
29574
  const {
29720
29575
  dragging
29721
- } = useMovable({
29576
+ } = useDraggable({
29722
29577
  gestureTarget: ref,
29723
29578
  point: dragReferencePoint,
29724
29579
  onMove: newPoint => {
@@ -29881,7 +29736,7 @@ function MovableCircle(props) {
29881
29736
  const draggableRef = useRef(null);
29882
29737
  const {
29883
29738
  dragging
29884
- } = useMovable({
29739
+ } = useDraggable({
29885
29740
  gestureTarget: draggableRef,
29886
29741
  point: center,
29887
29742
  onMove,
@@ -30012,10 +29867,24 @@ function SinusoidGraph(props) {
30012
29867
  coords
30013
29868
  } = graphState;
30014
29869
 
30015
- // Get the coefficients for calculating the quadratic equation
29870
+ // The coefficients are used to calculate the sinusoid equation, plot the graph, and to indicate
29871
+ // to content creators the currently selected "correct answer" in the Content Editor.
29872
+ // While we should technically never have invalid coordinates, we want to ensure that
29873
+ // we have a fallback so that the graph can still be plotted without crashing.
29874
+ const coeffRef = React.useRef({
29875
+ amplitude: 1,
29876
+ angularFrequency: 1,
29877
+ phase: 1,
29878
+ verticalOffset: 0
29879
+ });
30016
29880
  const coeffs = getSinusoidCoefficients(coords);
29881
+
29882
+ // If the coefficients are valid, update the reference
29883
+ if (coeffs !== undefined) {
29884
+ coeffRef.current = coeffs;
29885
+ }
30017
29886
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$3.OfX, {
30018
- y: x => computeSine(x, coeffs),
29887
+ y: x => computeSine(x, coeffRef.current),
30019
29888
  color: color.blue
30020
29889
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
30021
29890
  key: "point-" + i,
@@ -30042,6 +29911,11 @@ const getSinusoidCoefficients = coords => {
30042
29911
  const p1 = coords[0];
30043
29912
  const p2 = coords[1];
30044
29913
 
29914
+ // If the x-coordinates are the same, we are unable to calculate the coefficients
29915
+ if (p2[0] === p1[0]) {
29916
+ return;
29917
+ }
29918
+
30045
29919
  // Resulting coefficients are canonical for this sine curve
30046
29920
  const amplitude = p2[1] - p1[1];
30047
29921
  const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
@@ -31515,11 +31389,14 @@ function doMovePoint(state, action) {
31515
31389
  }
31516
31390
  case "sinusoid":
31517
31391
  {
31518
- // First, we need to verify that the new coordinates are not on the same vertical line
31519
- // If they are, we don't want to move the point
31392
+ // First, we need to make sure to bound the new coordinates to the graph range
31520
31393
  const destination = action.destination;
31394
+ const boundDestination = boundAndSnapToGrid(destination, state);
31395
+
31396
+ // Then, we need to verify that the new coordinates are not on the same
31397
+ // vertical line. If they are, then we don't want to move the point
31521
31398
  const _newCoords = [...state.coords];
31522
- _newCoords[action.index] = action.destination;
31399
+ _newCoords[action.index] = boundDestination;
31523
31400
  if (_newCoords[0][0] === _newCoords[1][0]) {
31524
31401
  return state;
31525
31402
  }
@@ -31528,7 +31405,7 @@ function doMovePoint(state, action) {
31528
31405
  coords: setAtIndex({
31529
31406
  array: state.coords,
31530
31407
  index: action.index,
31531
- newValue: boundAndSnapToGrid(destination, state)
31408
+ newValue: boundDestination
31532
31409
  })
31533
31410
  });
31534
31411
  }
@@ -31536,7 +31413,10 @@ function doMovePoint(state, action) {
31536
31413
  {
31537
31414
  // Set up the new coords and check if the quadratic coefficients are valid
31538
31415
  const _newCoords2 = [...state.coords];
31539
- _newCoords2[action.index] = action.destination;
31416
+
31417
+ // Bind the new destination to the graph range/snapStep and then get the quadratic coefficients
31418
+ const boundDestination = boundAndSnapToGrid(action.destination, state);
31419
+ _newCoords2[action.index] = boundDestination;
31540
31420
  const QuadraticCoefficients = getQuadraticCoefficients(_newCoords2);
31541
31421
 
31542
31422
  // If the new destination results in an invalid quadratic equation, we don't want to move the point
@@ -31548,7 +31428,7 @@ function doMovePoint(state, action) {
31548
31428
  coords: setAtIndex({
31549
31429
  array: state.coords,
31550
31430
  index: action.index,
31551
- newValue: boundAndSnapToGrid(action.destination, state)
31431
+ newValue: boundDestination
31552
31432
  })
31553
31433
  });
31554
31434
  }
@@ -31975,6 +31855,10 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
31975
31855
  dispatch(changeRange([[xMinRange, xMaxRange], [yMinRange, yMaxRange]]));
31976
31856
  }, [dispatch, xMinRange, xMaxRange, yMinRange, yMaxRange]);
31977
31857
  const numSegments = graph.type === "segment" ? graph.numSegments : null;
31858
+ const numSides = graph.type === "polygon" ? graph.numSides : null;
31859
+ const snapTo = graph.type === "polygon" ? graph.snapTo : null;
31860
+ const showAngles = graph.type === "polygon" ? graph.showAngles : null;
31861
+ const showSides = graph.type === "polygon" ? graph.showSides : null;
31978
31862
  const originalPropsRef = useRef(props);
31979
31863
  const latestPropsRef = useLatestRef(props);
31980
31864
  useEffect(() => {
@@ -31985,7 +31869,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
31985
31869
  if (latestPropsRef.current !== originalPropsRef.current) {
31986
31870
  dispatch(reinitialize(latestPropsRef.current));
31987
31871
  }
31988
- }, [graph.type, numSegments, latestPropsRef]);
31872
+ }, [graph.type, numSegments, numSides, snapTo, showAngles, showSides, latestPropsRef]);
31989
31873
  return /*#__PURE__*/React.createElement(MafsGraph, _extends({}, props, {
31990
31874
  state: state,
31991
31875
  dispatch: dispatch
@@ -33360,6 +33244,12 @@ class InteractiveGraph extends React.Component {
33360
33244
  }
33361
33245
  render() {
33362
33246
  var _this$props$apiOption;
33247
+ if (this.props.graph.type === "polygon" && this.props.graph.numSides === "unlimited") {
33248
+ return /*#__PURE__*/React.createElement(LegacyInteractiveGraph, _extends({
33249
+ ref: this.legacyGraphRef
33250
+ }, this.props));
33251
+ }
33252
+
33363
33253
  // Mafs shim
33364
33254
  if ((_this$props$apiOption = this.props.apiOptions) != null && (_this$props$apiOption = _this$props$apiOption.flags) != null && (_this$props$apiOption = _this$props$apiOption["mafs"]) != null && _this$props$apiOption[this.props.graph.type]) {
33365
33255
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -44152,7 +44042,7 @@ const init = function init(options) {
44152
44042
 
44153
44043
  // This file is processed by a Rollup plugin (replace) to inject the production
44154
44044
  const libName = "@khanacademy/perseus";
44155
- const libVersion = "23.5.0";
44045
+ const libVersion = "24.0.0";
44156
44046
  addLibraryVersionToPerseusDebug(libName, libVersion);
44157
44047
 
44158
44048
  const apiVersion = {