@khanacademy/perseus 47.0.0 → 48.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
@@ -2287,9 +2287,6 @@ function scoreExpression(userInput, rubric, strings, locale) {
2287
2287
  // We matched a graded answer form, so we can now tell the user
2288
2288
  // whether their input was correct or incorrect, and hand out
2289
2289
  // points accordingly
2290
- // TODO(eater): Seems silly to translate result to this
2291
- // invalid/points thing and immediately translate it back in
2292
- // ItemRenderer.scoreInput()
2293
2290
  return {
2294
2291
  type: "points",
2295
2292
  earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
@@ -3240,7 +3237,7 @@ const supportsPassiveEvents = () => {
3240
3237
  window.addEventListener("testPassive", null, opts);
3241
3238
  // @ts-expect-error - TS2769 - No overload matches this call.
3242
3239
  window.removeEventListener("testPassive", null, opts);
3243
- } catch (e) {
3240
+ } catch (_unused) {
3244
3241
  // Intentionally left empty!
3245
3242
  }
3246
3243
  return supportsPassive;
@@ -3483,7 +3480,9 @@ function changeViewportTag(contentString, callback) {
3483
3480
  // finished resetting.
3484
3481
  // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
3485
3482
  // eslint-disable-next-line no-restricted-syntax
3486
- callback && setTimeout(callback, 0);
3483
+ if (callback != null) {
3484
+ setTimeout(callback, 0);
3485
+ }
3487
3486
  }, 0);
3488
3487
  }
3489
3488
 
@@ -16768,7 +16767,7 @@ class ImageLoader extends React.Component {
16768
16767
  onKeyUp = e => {
16769
16768
  // 13 is enter key, 32 is space key
16770
16769
  if (e.keyCode === 13 || e.keyCode === 32) {
16771
- imgProps.onClick && imgProps.onClick(e);
16770
+ imgProps.onClick == null || imgProps.onClick(e);
16772
16771
  }
16773
16772
  };
16774
16773
  onKeyDown = e => {
@@ -16849,6 +16848,7 @@ const labelDataCache = {};
16849
16848
  // Write our own JSONP handler because all the other ones don't do things we
16850
16849
  // need.
16851
16850
  const doJSONP = function doJSONP(url, options) {
16851
+ var _document$head2;
16852
16852
  options = _extends({
16853
16853
  callbackName: "callback",
16854
16854
  success: $.noop,
@@ -16862,7 +16862,8 @@ const doJSONP = function doJSONP(url, options) {
16862
16862
 
16863
16863
  // A cleanup function to run when we're done.
16864
16864
  function cleanup() {
16865
- document.head && document.head.removeChild(script);
16865
+ var _document$head;
16866
+ (_document$head = document.head) == null || _document$head.removeChild(script);
16866
16867
  delete window[options.callbackName];
16867
16868
  }
16868
16869
 
@@ -16880,7 +16881,7 @@ const doJSONP = function doJSONP(url, options) {
16880
16881
  });
16881
16882
 
16882
16883
  // Insert the script to start the download.
16883
- document.head && document.head.appendChild(script);
16884
+ (_document$head2 = document.head) == null || _document$head2.appendChild(script);
16884
16885
  };
16885
16886
 
16886
16887
  // For offline exercises in the mobile app, we download the graphie data
@@ -17050,6 +17051,7 @@ class SvgImage extends React.Component {
17050
17051
  });
17051
17052
  };
17052
17053
  this._handleZoomClick = e => {
17054
+ var _this$props$trackInte, _this$props;
17053
17055
  const $image = $(e.target);
17054
17056
 
17055
17057
  // It's possible that the image is already displayed at its
@@ -17066,7 +17068,7 @@ class SvgImage extends React.Component {
17066
17068
  $image.width() < this.props.width || this.props.zoomToFullSizeOnMobile) {
17067
17069
  ZoomService.handleZoomClick(e, this.props.zoomToFullSizeOnMobile);
17068
17070
  }
17069
- this.props.trackInteraction && this.props.trackInteraction();
17071
+ (_this$props$trackInte = (_this$props = this.props).trackInteraction) == null || _this$props$trackInte.call(_this$props);
17070
17072
  };
17071
17073
  this.handleUpdate = status => {
17072
17074
  this.props.onUpdate();
@@ -19602,8 +19604,8 @@ class WidgetContainer extends React.Component {
19602
19604
  });
19603
19605
 
19604
19606
  // Note: if you add more props here, please consider whether or not
19605
- // it should be auto-serialized (e.g. used in scoreInput()). See
19606
- // widget-jsonify-deprecated.jsx and widget-prop-denylist.jsx
19607
+ // it should be auto-serialized.
19608
+ // See widget-jsonify-deprecated.jsx and widget-prop-denylist.jsx
19607
19609
 
19608
19610
  // We default to an empty object for style instead of null
19609
19611
  // because of a strange bug where the static styles aren't applied
@@ -20582,14 +20584,6 @@ class Renderer extends React.Component {
20582
20584
  this.getWidgetIds = () => {
20583
20585
  return this.widgetIds;
20584
20586
  };
20585
- /**
20586
- * @deprecated use scorePerseusItem
20587
- */
20588
- this.guessAndScore = () => {
20589
- const totalGuess = this.getUserInput();
20590
- const totalScore = this.score();
20591
- return [totalGuess, totalScore];
20592
- };
20593
20587
  // TranslationLinter callback
20594
20588
  this.handletranslationLintErrors = lintErrors => {
20595
20589
  if (!this._isMounted) {
@@ -21556,11 +21550,11 @@ class SimpleKeypadInput extends React.Component {
21556
21550
  keypadType: KeypadType.FRACTION
21557
21551
  }, () => {
21558
21552
  if (_this._isMounted) {
21559
- onFocus && onFocus();
21553
+ onFocus == null || onFocus();
21560
21554
  }
21561
21555
  });
21562
21556
  } else {
21563
- onFocus && onFocus();
21557
+ onFocus == null || onFocus();
21564
21558
  }
21565
21559
  },
21566
21560
  value: value == null ? "" : "" + value
@@ -21799,9 +21793,6 @@ function scoreInputNumber(userInput, rubric, strings) {
21799
21793
  // If `currentValue` is not TeX, this should be a no-op.
21800
21794
  const currentValue = ParseTex$1(userInput.currentValue);
21801
21795
  const result = val(currentValue);
21802
-
21803
- // TODO(eater): Seems silly to translate result to this invalid/points
21804
- // thing and immediately translate it back in ItemRenderer.scoreInput()
21805
21796
  if (result.empty) {
21806
21797
  return {
21807
21798
  type: "invalid",
@@ -22168,10 +22159,6 @@ function scoreNumericInput(userInput, rubric, strings) {
22168
22159
  message: (_matchedAnswer$messag = matchedAnswer == null ? void 0 : matchedAnswer.message) != null ? _matchedAnswer$messag : null,
22169
22160
  guess: localValue
22170
22161
  };
22171
-
22172
- // TODO(eater): Seems silly to translate result to this
22173
- // invalid/points thing and immediately translate it
22174
- // back in ItemRenderer.scoreInput()
22175
22162
  if (result.empty) {
22176
22163
  return {
22177
22164
  type: "invalid",
@@ -25024,7 +25011,7 @@ const scrollElementToOffset = (elementOrWindow, offset, duration = 0, callback)
25024
25011
  // TODO(jeff, WEB-1378): Use Wonder Blocks Timing API
25025
25012
  // eslint-disable-next-line no-restricted-syntax
25026
25013
  clearInterval(scrollInterval);
25027
- callback && callback();
25014
+ callback == null || callback();
25028
25015
  }
25029
25016
  }, tickLength);
25030
25017
  };
@@ -27048,7 +27035,7 @@ class CSProgram extends React.Component {
27048
27035
  let data = {};
27049
27036
  try {
27050
27037
  data = JSON.parse(e.originalEvent.data);
27051
- } catch (err) {
27038
+ } catch (_unused) {
27052
27039
  return;
27053
27040
  }
27054
27041
  if (_.isUndefined(data.testsPassed)) {
@@ -27350,7 +27337,7 @@ class Dropdown extends React.Component {
27350
27337
  key: "placeholder",
27351
27338
  value: "0",
27352
27339
  disabled: true,
27353
- label: this.props.placeholder
27340
+ label: this.props.placeholder || " "
27354
27341
  }), ...this.props.choices.map((choice, i) => /*#__PURE__*/React.createElement(OptionItem, {
27355
27342
  key: String(i + 1),
27356
27343
  value: String(i + 1),
@@ -27371,15 +27358,14 @@ class Dropdown extends React.Component {
27371
27358
  }
27372
27359
  }, this.props.visibleLabel && /*#__PURE__*/React.createElement(LabelLarge, {
27373
27360
  tag: "label",
27374
- id: ids.get("dropdown-label")
27361
+ htmlFor: ids.get("dropdown")
27375
27362
  }, this.props.visibleLabel), /*#__PURE__*/React.createElement(SingleSelect, {
27376
27363
  id: ids.get("dropdown"),
27377
27364
  placeholder: "",
27378
27365
  onChange: value => this._handleChange(parseInt(value)),
27379
27366
  selectedValue: String(this.props.selected),
27380
27367
  disabled: this.props.apiOptions.readOnly,
27381
- "aria-label": this.props.ariaLabel || this.context.strings.selectAnAnswer,
27382
- "aria-labelledby": ids.get("dropdown-label")
27368
+ "aria-label": this.props.ariaLabel || this.props.visibleLabel || this.context.strings.selectAnAnswer
27383
27369
  // This is currently necessary for SRs to read the labels properly.
27384
27370
  // However, WB is working on a change to add the "combobox" role to
27385
27371
  // all dropdowns.
@@ -28105,7 +28091,6 @@ class GradedGroupSet extends React.Component {
28105
28091
  };
28106
28092
  }
28107
28093
  shouldComponentUpdate(nextProps, nextState) {
28108
- nextProps.gradedGroups;
28109
28094
  return nextProps !== this.props || nextState !== this.state;
28110
28095
  }
28111
28096
  getPromptJSON() {
@@ -29574,7 +29559,7 @@ class Iframe extends React.Component {
29574
29559
  let data = {};
29575
29560
  try {
29576
29561
  data = JSON.parse(e.originalEvent.data);
29577
- } catch (err) {
29562
+ } catch (_unused) {
29578
29563
  return;
29579
29564
  }
29580
29565
  if (_.isUndefined(data.testsPassed)) {
@@ -30639,10 +30624,12 @@ class Graph extends React.Component {
30639
30624
  this.props.onMouseDown(coord);
30640
30625
  }, this) : undefined;
30641
30626
  const onMouseOver = $instructionsWrapper ? function () {
30642
- $instructionsWrapper && $instructionsWrapper.css("opacity", invisible);
30627
+ var _$instructionsWrapper;
30628
+ (_$instructionsWrapper = $instructionsWrapper) == null || _$instructionsWrapper.css("opacity", invisible);
30643
30629
  } : undefined;
30644
30630
  const onMouseOut = $instructionsWrapper ? function () {
30645
- $instructionsWrapper && $instructionsWrapper.css("opacity", visible);
30631
+ var _$instructionsWrapper2;
30632
+ (_$instructionsWrapper2 = $instructionsWrapper) == null || _$instructionsWrapper2.css("opacity", visible);
30646
30633
  } : undefined;
30647
30634
  /* eslint-enable indent */
30648
30635
 
@@ -31374,6 +31361,7 @@ function replaceOutsideTeX(mathString) {
31374
31361
  const parsed = mathOnlyParser(mathString);
31375
31362
  let result = "";
31376
31363
  for (const piece of parsed) {
31364
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
31377
31365
  piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
31378
31366
  }
31379
31367
  return `\\text{${result}}`;
@@ -32223,11 +32211,39 @@ const LegacyGrid = ({
32223
32211
  return null;
32224
32212
  };
32225
32213
 
32214
+ /**
32215
+ * The Perseus "data schema" file.
32216
+ *
32217
+ * This file, and the types in it, represents the "data schema" that Perseus
32218
+ * uses. The @khanacademy/perseus-editor package edits and produces objects
32219
+ * that conform to the types in this file. Similarly, the top-level renderers
32220
+ * in @khanacademy/perseus, consume objects that conform to these types.
32221
+ *
32222
+ * WARNING: This file should not import any types from elsewhere so that it is
32223
+ * easy to reason about changes that alter the Perseus schema. This helps
32224
+ * ensure that it is not changed accidentally when upgrading a dependant
32225
+ * package or other part of Perseus code. Note that TypeScript does type
32226
+ * checking via something called "structural typing". This means that as long
32227
+ * as the shape of a type matches, the name it goes by doesn't matter. As a
32228
+ * result, a `Coord` type that looks like this `[x: number, y: number]` is
32229
+ * _identical_, in TypeScript's eyes, to this `Vector2` type `[x: number, y:
32230
+ * number]`. Also, with tuples, the labels for each entry is ignored, so `[x:
32231
+ * number, y: number]` is compatible with `[min: number, max: number]`. The
32232
+ * labels are for humans, not TypeScript. :)
32233
+ *
32234
+ * If you make changes to types in this file, be very sure that:
32235
+ *
32236
+ * a) the changes are backwards compatible. If they are not, old data from
32237
+ * previous versions of the "schema" could become unrenderable, or worse,
32238
+ * introduce hard-to-diagnose bugs.
32239
+ * b) the parsing code (`util/parse-perseus-json/`) is updated to handle
32240
+ * the new format _as well as_ the old format.
32241
+ */
32242
+
32226
32243
  // TODO(FEI-4010): Remove `Perseus` prefix for all types here
32227
- // TODO(FEI-4011): Use types generated by https://github.com/jaredly/generate-perseus-flowtypes
32228
32244
 
32229
- // Range is replaced within this file with Interval, but it is used elsewhere
32230
- // and exported from the package, so we need to keep it around.
32245
+ // Same name as Mafs
32246
+
32231
32247
  /**
32232
32248
  * Our core set of Perseus widgets.
32233
32249
  *
@@ -32271,6 +32287,7 @@ const LegacyGrid = ({
32271
32287
  *
32272
32288
  * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
32273
32289
  */
32290
+
32274
32291
  /**
32275
32292
  * A map of widget IDs to widget options. This is most often used as the type
32276
32293
  * for a set of widgets defined in a `PerseusItem` but can also be useful to
@@ -32283,6 +32300,7 @@ const LegacyGrid = ({
32283
32300
  * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
32284
32301
  * by augmenting the PerseusWidgetTypes with new widget types!
32285
32302
  */
32303
+
32286
32304
  /**
32287
32305
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32288
32306
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32290,10 +32308,12 @@ const LegacyGrid = ({
32290
32308
  * To render more complex Perseus items, see the `Item` type in the multi item
32291
32309
  * area.
32292
32310
  */
32311
+
32293
32312
  /**
32294
32313
  * A "PerseusArticle" is an item that is meant to be rendered as an article.
32295
32314
  * This item is never scored and is rendered by the `ArticleRenderer`.
32296
32315
  */
32316
+
32297
32317
  const ItemExtras = [
32298
32318
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
32299
32319
  "calculator",
@@ -34292,43 +34312,64 @@ function renderPointGraph(state, dispatch) {
34292
34312
  };
34293
34313
  }
34294
34314
  function PointGraph(props) {
34295
- const numPoints = props.graphState.numPoints;
34315
+ const {
34316
+ numPoints
34317
+ } = props.graphState;
34318
+ const graphConfig = useGraphConfig();
34319
+ const pointsRef = React.useRef([]);
34320
+
34321
+ // Dimensions to build the graph overlay for Unlimited Point.
34322
+ const {
34323
+ range: [x, y]
34324
+ } = graphConfig;
34325
+ const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
34326
+
34327
+ // This useEffect is to handle the focus snapping for Unlimited Point.
34328
+ React.useEffect(() => {
34329
+ const focusedIndex = props.graphState.focusedPointIndex;
34330
+ if (focusedIndex != null) {
34331
+ var _pointsRef$current$fo;
34332
+ (_pointsRef$current$fo = pointsRef.current[focusedIndex]) == null || _pointsRef$current$fo.focus();
34333
+ }
34334
+ }, [props.graphState.focusedPointIndex, pointsRef]);
34335
+ const statefulProps = _extends({}, props, {
34336
+ graphConfig,
34337
+ pointsRef,
34338
+ top,
34339
+ left
34340
+ });
34296
34341
  if (numPoints === "unlimited") {
34297
- return UnlimitedPointGraph(props);
34342
+ return UnlimitedPointGraph(statefulProps);
34298
34343
  }
34299
- return LimitedPointGraph(props);
34344
+ return LimitedPointGraph(statefulProps);
34300
34345
  }
34301
- function LimitedPointGraph(props) {
34346
+ function LimitedPointGraph(statefulProps) {
34302
34347
  const {
34303
34348
  dispatch
34304
- } = props;
34305
- return /*#__PURE__*/React.createElement(React.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34349
+ } = statefulProps;
34350
+ return /*#__PURE__*/React.createElement(React.Fragment, null, statefulProps.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34306
34351
  key: i,
34307
34352
  point: point,
34308
34353
  sequenceNumber: i + 1,
34309
34354
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34310
34355
  })));
34311
34356
  }
34312
- function UnlimitedPointGraph(props) {
34357
+ function UnlimitedPointGraph(statefulProps) {
34313
34358
  const {
34314
- dispatch
34315
- } = props;
34316
- const graphConfig = useGraphConfig();
34359
+ dispatch,
34360
+ graphConfig,
34361
+ pointsRef,
34362
+ top,
34363
+ left
34364
+ } = statefulProps;
34365
+ const {
34366
+ coords
34367
+ } = statefulProps.graphState;
34317
34368
  const {
34318
- range: [[minX, maxX], [minY, maxY]]
34369
+ graphDimensionsInPixels
34319
34370
  } = graphConfig;
34320
- const width = maxX - minX;
34321
- const height = maxY - minY;
34322
- const [[widthPx, heightPx]] = useTransformDimensionsToPixels([width, height]);
34323
- const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
34324
- const itemsRef = React.useRef([]);
34325
- React.useEffect(() => {
34326
- const focusedIndex = props.graphState.focusedPointIndex;
34327
- if (focusedIndex != null) {
34328
- var _itemsRef$current$foc;
34329
- (_itemsRef$current$foc = itemsRef.current[focusedIndex]) == null || _itemsRef$current$foc.focus();
34330
- }
34331
- }, [props.graphState.focusedPointIndex, itemsRef]);
34371
+ const widthPx = graphDimensionsInPixels[0];
34372
+ const heightPx = graphDimensionsInPixels[1];
34332
34373
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
34333
34374
  style: {
34334
34375
  fill: "rgba(0,0,0,0)",
@@ -34345,13 +34386,13 @@ function UnlimitedPointGraph(props) {
34345
34386
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34346
34387
  dispatch(actions.pointGraph.addPoint(graphCoordinates[0]));
34347
34388
  }
34348
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34389
+ }), coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34349
34390
  key: i,
34350
34391
  point: point,
34351
34392
  sequenceNumber: i + 1,
34352
34393
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34353
34394
  ref: ref => {
34354
- itemsRef.current[i] = ref;
34395
+ pointsRef.current[i] = ref;
34355
34396
  },
34356
34397
  onFocus: () => {
34357
34398
  dispatch(actions.pointGraph.focusPoint(i));
@@ -34398,36 +34439,42 @@ function renderPolygonGraph(state, dispatch) {
34398
34439
  interactiveElementsDescription: null
34399
34440
  };
34400
34441
  }
34401
- const LimitedPolygonGraph = props => {
34402
- const [hovered, setHovered] = React.useState(false);
34403
- // This is more so required for the re-rendering that occurs when state
34404
- // updates; specifically with regard to line weighting and polygon focus.
34405
- const [focusVisible, setFocusVisible] = React.useState(false);
34442
+ const PolygonGraph = props => {
34406
34443
  const {
34407
34444
  dispatch
34408
34445
  } = props;
34409
34446
  const {
34447
+ numSides,
34410
34448
  coords,
34411
- showAngles,
34412
- showSides,
34413
- range,
34414
34449
  snapStep,
34415
34450
  snapTo = "grid"
34416
34451
  } = props.graphState;
34452
+ const graphConfig = useGraphConfig();
34453
+
34454
+ // Ref to implement the dragging behavior on a Limited/Closed Polygon.
34455
+ const polygonRef = React.useRef(null);
34456
+ // Ref to manage dynamic focus for the points in Unlimited Polygon
34457
+ const pointsRef = React.useRef([]);
34458
+ // Ref to manage the last move time for a Limited Polygon.
34459
+ const lastMoveTimeRef = React.useRef(0);
34460
+
34461
+ // Dimensions to build the graph overlay for Unlimited Polgyon.
34417
34462
  const {
34418
- disableKeyboardInteraction
34419
- } = useGraphConfig();
34463
+ range: [x, y]
34464
+ } = graphConfig;
34465
+ const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
34420
34466
 
34421
34467
  // TODO(benchristel): can the default set of points be removed here? I don't
34422
34468
  // think coords can be null.
34423
34469
  const points = coords != null ? coords : [[0, 0]];
34424
- const ref = React.useRef(null);
34470
+
34471
+ // Logic to build the dragging experience. Primarily used by Limited Polygon.
34425
34472
  const dragReferencePoint = points[0];
34426
34473
  const constrain = ["angles", "sides"].includes(snapTo) ? p => p : p => snap(snapStep, p);
34427
34474
  const {
34428
34475
  dragging
34429
34476
  } = useDraggable({
34430
- gestureTarget: ref,
34477
+ gestureTarget: polygonRef,
34431
34478
  point: dragReferencePoint,
34432
34479
  onMove: newPoint => {
34433
34480
  const delta = vec.sub(newPoint, dragReferencePoint);
@@ -34435,7 +34482,61 @@ const LimitedPolygonGraph = props => {
34435
34482
  },
34436
34483
  constrainKeyboardMovement: constrain
34437
34484
  });
34438
- const lastMoveTime = React.useRef(0);
34485
+
34486
+ // Hover/focus state for Limited Polgyon effects.
34487
+ const [hovered, setHovered] = React.useState(false);
34488
+ // This is more so required for the re-rendering that occurs when state
34489
+ // updates; specifically with regard to line weighting and polygon focus.
34490
+ const [focusVisible, setFocusVisible] = React.useState(false);
34491
+
34492
+ // This useEffect is to handle the focus snapping for Unlimited Polygon.
34493
+ React.useEffect(() => {
34494
+ const focusedIndex = props.graphState.focusedPointIndex;
34495
+ if (focusedIndex != null) {
34496
+ var _pointsRef$current$fo;
34497
+ (_pointsRef$current$fo = pointsRef.current[focusedIndex]) == null || _pointsRef$current$fo.focus();
34498
+ }
34499
+ }, [props.graphState.focusedPointIndex, pointsRef]);
34500
+ const statefulProps = _extends({}, props, {
34501
+ graphConfig,
34502
+ polygonRef,
34503
+ pointsRef,
34504
+ lastMoveTimeRef,
34505
+ left,
34506
+ top,
34507
+ dragging,
34508
+ points,
34509
+ constrain,
34510
+ hovered,
34511
+ setHovered,
34512
+ focusVisible,
34513
+ setFocusVisible
34514
+ });
34515
+ return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
34516
+ };
34517
+ const LimitedPolygonGraph = statefulProps => {
34518
+ const {
34519
+ dispatch,
34520
+ hovered,
34521
+ setHovered,
34522
+ focusVisible,
34523
+ setFocusVisible,
34524
+ graphConfig,
34525
+ polygonRef,
34526
+ lastMoveTimeRef,
34527
+ dragging,
34528
+ points,
34529
+ constrain
34530
+ } = statefulProps;
34531
+ const {
34532
+ showAngles,
34533
+ showSides,
34534
+ range,
34535
+ snapTo = "grid"
34536
+ } = statefulProps.graphState;
34537
+ const {
34538
+ disableKeyboardInteraction
34539
+ } = graphConfig;
34439
34540
  const lines = getLines(points);
34440
34541
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polygon, {
34441
34542
  points: [...points],
@@ -34473,7 +34574,7 @@ const LimitedPolygonGraph = props => {
34473
34574
  points: [...points],
34474
34575
  color: "transparent",
34475
34576
  svgPolygonProps: {
34476
- ref,
34577
+ ref: polygonRef,
34477
34578
  tabIndex: disableKeyboardInteraction ? -1 : 0,
34478
34579
  strokeWidth: TARGET_SIZE,
34479
34580
  style: {
@@ -34485,13 +34586,13 @@ const LimitedPolygonGraph = props => {
34485
34586
  // Required to remove line weighting when user clicks away
34486
34587
  // from the focused polygon
34487
34588
  onKeyDownCapture: () => {
34488
- setFocusVisible(hasFocusVisible(ref.current));
34589
+ setFocusVisible(hasFocusVisible(polygonRef.current));
34489
34590
  },
34490
34591
  // Required for lines to darken on focus
34491
- onFocus: () => setFocusVisible(hasFocusVisible(ref.current)),
34592
+ onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
34492
34593
  // Required for line weighting to update on blur. Without this,
34493
34594
  // the user has to hover over the shape for it to update
34494
- onBlur: () => setFocusVisible(hasFocusVisible(ref.current)),
34595
+ onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
34495
34596
  className: "movable-polygon"
34496
34597
  }
34497
34598
  }), points.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
@@ -34503,94 +34604,85 @@ const LimitedPolygonGraph = props => {
34503
34604
  const now = Date.now();
34504
34605
  const targetFPS = 40;
34505
34606
  const moveThresholdTime = 1000 / targetFPS;
34506
- if (now - lastMoveTime.current > moveThresholdTime) {
34607
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
34507
34608
  dispatch(actions.polygon.movePoint(i, destination));
34508
- lastMoveTime.current = now;
34609
+ lastMoveTimeRef.current = now;
34509
34610
  }
34510
34611
  }
34511
34612
  })));
34512
34613
  };
34513
- const UnlimitedPolygonGraph = props => {
34614
+ const UnlimitedPolygonGraph = statefulProps => {
34514
34615
  const {
34515
- dispatch
34516
- } = props;
34616
+ dispatch,
34617
+ graphConfig,
34618
+ left,
34619
+ top,
34620
+ pointsRef,
34621
+ points
34622
+ } = statefulProps;
34517
34623
  const {
34518
34624
  coords,
34519
34625
  closedPolygon
34520
- } = props.graphState;
34521
- const graphConfig = useGraphConfig();
34626
+ } = statefulProps.graphState;
34627
+
34628
+ // If the polygon is closed, return a LimitedPolygon component.
34629
+ if (closedPolygon) {
34630
+ const closedPolygonProps = _extends({}, statefulProps, {
34631
+ numSides: coords.length
34632
+ });
34633
+ return /*#__PURE__*/React.createElement(LimitedPolygonGraph, closedPolygonProps);
34634
+ }
34522
34635
  const {
34523
- range: [x, y],
34524
34636
  graphDimensionsInPixels
34525
34637
  } = graphConfig;
34526
34638
  const widthPx = graphDimensionsInPixels[0];
34527
34639
  const heightPx = graphDimensionsInPixels[1];
34528
- const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
34529
- const pointRef = React.useRef([]);
34530
-
34531
- // TODO(benchristel): can the default set of points be removed here? I don't
34532
- // think coords can be null.
34533
- const points = coords != null ? coords : [[0, 0]];
34534
- React.useEffect(() => {
34535
- const focusedIndex = props.graphState.focusedPointIndex;
34536
- if (focusedIndex != null) {
34537
- var _pointRef$current$foc;
34538
- (_pointRef$current$foc = pointRef.current[focusedIndex]) == null || _pointRef$current$foc.focus();
34640
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
34641
+ style: {
34642
+ fill: "rgba(0,0,0,0)",
34643
+ cursor: "crosshair"
34644
+ },
34645
+ width: widthPx,
34646
+ height: heightPx,
34647
+ x: left,
34648
+ y: top,
34649
+ onClick: event => {
34650
+ const elementRect = event.currentTarget.getBoundingClientRect();
34651
+ const x = event.clientX - elementRect.x;
34652
+ const y = event.clientY - elementRect.y;
34653
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34654
+ dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34539
34655
  }
34540
- }, [props.graphState.focusedPointIndex, pointRef]);
34541
- if (closedPolygon) {
34542
- const closedPolygonProps = _extends({}, props, {
34543
- numSides: coords.length
34544
- });
34545
- return /*#__PURE__*/React.createElement(LimitedPolygonGraph, closedPolygonProps);
34546
- } else {
34547
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
34656
+ }), /*#__PURE__*/React.createElement(Polyline, {
34657
+ points: [...points],
34658
+ color: "var(--movable-line-stroke-color)",
34659
+ svgPolylineProps: {
34660
+ strokeWidth: "var(--movable-line-stroke-weight)",
34548
34661
  style: {
34549
- fill: "rgba(0,0,0,0)",
34550
- cursor: "crosshair"
34551
- },
34552
- width: widthPx,
34553
- height: heightPx,
34554
- x: left,
34555
- y: top,
34556
- onClick: event => {
34557
- const elementRect = event.currentTarget.getBoundingClientRect();
34558
- const x = event.clientX - elementRect.x;
34559
- const y = event.clientY - elementRect.y;
34560
- const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34561
- dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34562
- }
34563
- }), /*#__PURE__*/React.createElement(Polyline, {
34564
- points: [...points],
34565
- color: "var(--movable-line-stroke-color)",
34566
- svgPolylineProps: {
34567
- strokeWidth: "var(--movable-line-stroke-weight)",
34568
- style: {
34569
- fill: "transparent"
34570
- }
34662
+ fill: "transparent"
34571
34663
  }
34572
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34573
- key: i,
34574
- point: point,
34575
- sequenceNumber: i + 1,
34576
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34577
- ref: ref => {
34578
- pointRef.current[i] = ref;
34579
- },
34580
- onFocus: () => {
34581
- dispatch(actions.polygon.focusPoint(i));
34582
- },
34583
- onClick: () => {
34584
- // If the point being clicked is the first point and
34585
- // there's enough points to form a polygon (3 or more)
34586
- // Close the shape before setting focus.
34587
- if (i === 0 && props.graphState.coords.length >= 3) {
34588
- dispatch(actions.polygon.closePolygon());
34589
- }
34590
- dispatch(actions.polygon.clickPoint(i));
34664
+ }
34665
+ }), coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34666
+ key: i,
34667
+ point: point,
34668
+ sequenceNumber: i + 1,
34669
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34670
+ ref: ref => {
34671
+ pointsRef.current[i] = ref;
34672
+ },
34673
+ onFocus: () => {
34674
+ dispatch(actions.polygon.focusPoint(i));
34675
+ },
34676
+ onClick: () => {
34677
+ // If the point being clicked is the first point and
34678
+ // there's enough points to form a polygon (3 or more)
34679
+ // Close the shape before setting focus.
34680
+ if (i === 0 && coords.length >= 3) {
34681
+ dispatch(actions.polygon.closePolygon());
34591
34682
  }
34592
- })));
34593
- }
34683
+ dispatch(actions.polygon.clickPoint(i));
34684
+ }
34685
+ })));
34594
34686
  };
34595
34687
  function getLines(points) {
34596
34688
  return points.map((point, i) => {
@@ -34605,17 +34697,13 @@ const hasFocusVisible = element => {
34605
34697
  };
34606
34698
  try {
34607
34699
  return matches(":focus-visible");
34608
- } catch (e) {
34700
+ } catch (_unused) {
34609
34701
  // jsdom doesn't support :focus-visible
34610
34702
  // (see https://github.com/jsdom/jsdom/issues/3426),
34611
34703
  // so the call to matches(":focus-visible") will fail in tests.
34612
34704
  return matches(":focus");
34613
34705
  }
34614
34706
  };
34615
- const PolygonGraph = props => {
34616
- const numSides = props.graphState.numSides;
34617
- return numSides === "unlimited" ? UnlimitedPolygonGraph(props) : LimitedPolygonGraph(props);
34618
- };
34619
34707
 
34620
34708
  function renderQuadraticGraph(state, dispatch) {
34621
34709
  return {
@@ -39981,7 +40069,8 @@ class Sortable extends React.Component {
39981
40069
  this.setState({
39982
40070
  items
39983
40071
  }, () => {
39984
- this.props.onMeasure && this.props.onMeasure({
40072
+ var _this$props$onMeasure, _this$props;
40073
+ (_this$props$onMeasure = (_this$props = this.props).onMeasure) == null || _this$props$onMeasure.call(_this$props, {
39985
40074
  widths: widths,
39986
40075
  heights: heights
39987
40076
  });
@@ -40007,6 +40096,7 @@ class Sortable extends React.Component {
40007
40096
  // for testing and other cases where dragging and dropping may
40008
40097
  // not be available
40009
40098
  moveOptionToIndex(option, index) {
40099
+ var _this$props$onChange, _this$props2;
40010
40100
  const {
40011
40101
  items
40012
40102
  } = this.state;
@@ -40031,7 +40121,7 @@ class Sortable extends React.Component {
40031
40121
  this.setState({
40032
40122
  items: nextItems
40033
40123
  });
40034
- this.props.onChange && this.props.onChange({});
40124
+ (_this$props$onChange = (_this$props2 = this.props).onChange) == null || _this$props$onChange.call(_this$props2, {});
40035
40125
  }
40036
40126
  onMouseMove(key) {
40037
40127
  // Dragging: Rearrange items based on draggable's position
@@ -40089,6 +40179,7 @@ class Sortable extends React.Component {
40089
40179
  // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
40090
40180
  // eslint-disable-next-line no-restricted-syntax
40091
40181
  const nextAnimationFrame = requestAnimationFrame(() => {
40182
+ var _this$props$onChange2, _this$props3;
40092
40183
  const items = _.map(this.state.items, function (item) {
40093
40184
  if (item.key === key) {
40094
40185
  item.state = ItemState.ANIMATING;
@@ -40111,7 +40202,7 @@ class Sortable extends React.Component {
40111
40202
  // HACK: We need to know *that* the widget changed, but currently it's
40112
40203
  // not set up in a nice way to tell us *how* it changed, since the
40113
40204
  // permutation of the items is stored in state.
40114
- this.props.onChange && this.props.onChange({});
40205
+ (_this$props$onChange2 = (_this$props3 = this.props).onChange) == null || _this$props$onChange2.call(_this$props3, {});
40115
40206
  });
40116
40207
 
40117
40208
  // @ts-expect-error - TS2339 - Property 'animationFrameRequest' does not exist on type 'Sortable'.
@@ -42789,11 +42880,12 @@ class Card extends React.Component {
42789
42880
  event.preventDefault();
42790
42881
  const loc = Util.extractPointerLocation(event);
42791
42882
  if (loc) {
42883
+ var _this$props$onMouseDo, _this$props;
42792
42884
  this.setState({
42793
42885
  dragging: true
42794
42886
  });
42795
42887
  this.bindMouseMoveUp();
42796
- this.props.onMouseDown && this.props.onMouseDown(loc, this);
42888
+ (_this$props$onMouseDo = (_this$props = this.props).onMouseDown) == null || _this$props$onMouseDo.call(_this$props, loc, this);
42797
42889
  }
42798
42890
  };
42799
42891
  this.onMouseMove = event => {
@@ -42803,18 +42895,20 @@ class Card extends React.Component {
42803
42895
  event.preventDefault();
42804
42896
  const loc = Util.extractPointerLocation(event);
42805
42897
  if (loc) {
42806
- this.props.onMouseMove && this.props.onMouseMove(loc);
42898
+ var _this$props$onMouseMo, _this$props2;
42899
+ (_this$props$onMouseMo = (_this$props2 = this.props).onMouseMove) == null || _this$props$onMouseMo.call(_this$props2, loc);
42807
42900
  }
42808
42901
  };
42809
42902
  this.onMouseUp = event => {
42810
42903
  event.preventDefault();
42811
42904
  const loc = Util.extractPointerLocation(event);
42812
42905
  if (loc) {
42906
+ var _this$props$onMouseUp, _this$props3;
42813
42907
  this.setState({
42814
42908
  dragging: false
42815
42909
  });
42816
42910
  this.unbindMouseMoveUp();
42817
- this.props.onMouseUp && this.props.onMouseUp(loc);
42911
+ (_this$props$onMouseUp = (_this$props3 = this.props).onMouseUp) == null || _this$props$onMouseUp.call(_this$props3, loc);
42818
42912
  }
42819
42913
  };
42820
42914
  }
@@ -45076,7 +45170,7 @@ const init = function init() {
45076
45170
 
45077
45171
  // This file is processed by a Rollup plugin (replace) to inject the production
45078
45172
  const libName = "@khanacademy/perseus";
45079
- const libVersion = "47.0.0";
45173
+ const libVersion = "48.0.0";
45080
45174
  addLibraryVersionToPerseusDebug(libName, libVersion);
45081
45175
 
45082
45176
  const apiVersion = {
@@ -45838,28 +45932,6 @@ class ServerItemRenderer extends React.Component {
45838
45932
  return this.questionRenderer.getUserInputMap();
45839
45933
  }
45840
45934
 
45841
- /**
45842
- * Grades the item.
45843
- *
45844
- * @deprecated use scorePerseusItem
45845
- */
45846
- scoreInput() {
45847
- const guess = this.getUserInput();
45848
- const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45849
-
45850
- // Continue to include an empty guess for the now defunct answer area.
45851
- // TODO(alex): Check whether we rely on the format here for
45852
- // analyzing ProblemLogs. If not, remove this layer.
45853
- const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45854
- const keScore = keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45855
- const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45856
- this.setState({
45857
- questionCompleted: keScore.correct,
45858
- questionHighlightedWidgets: emptyQuestionAreaWidgets
45859
- });
45860
- return keScore;
45861
- }
45862
-
45863
45935
  /**
45864
45936
  * Returns an array of all widget IDs in the order they occur in
45865
45937
  * the question content.