@khanacademy/perseus 47.0.1 → 48.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.
package/dist/index.js CHANGED
@@ -2412,9 +2412,6 @@ function scoreExpression(userInput, rubric, strings, locale) {
2412
2412
  // We matched a graded answer form, so we can now tell the user
2413
2413
  // whether their input was correct or incorrect, and hand out
2414
2414
  // points accordingly
2415
- // TODO(eater): Seems silly to translate result to this
2416
- // invalid/points thing and immediately translate it back in
2417
- // ItemRenderer.scoreInput()
2418
2415
  return {
2419
2416
  type: "points",
2420
2417
  earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
@@ -3363,7 +3360,7 @@ const supportsPassiveEvents = () => {
3363
3360
  window.addEventListener("testPassive", null, opts);
3364
3361
  // @ts-expect-error - TS2769 - No overload matches this call.
3365
3362
  window.removeEventListener("testPassive", null, opts);
3366
- } catch (e) {
3363
+ } catch {
3367
3364
  // Intentionally left empty!
3368
3365
  }
3369
3366
  return supportsPassive;
@@ -3497,6 +3494,163 @@ const Util = {
3497
3494
  deepClone
3498
3495
  };
3499
3496
 
3497
+ // For offline exercises in the mobile app, we download the graphie data
3498
+ // (svgs and localized data files) and serve them from the local file
3499
+ // system (with file://). We replace urls that start with `web+graphie`
3500
+ // in the perseus json with this `file+graphie` prefix to indicate that
3501
+ // they should have the `file://` protocol instead of `https://`.
3502
+ const svgLocalLabelsRegex = /^file\+graphie:/;
3503
+ const hashRegex = /\/([^/]+)$/;
3504
+
3505
+ /**
3506
+ * A Graphie axis range.
3507
+ */
3508
+
3509
+ /**
3510
+ * A Graphie label.
3511
+ */
3512
+
3513
+ /**
3514
+ * The Graphie data (labels, etc.)
3515
+ */
3516
+
3517
+ function getLocale() {
3518
+ const {
3519
+ JIPT,
3520
+ kaLocale
3521
+ } = getDependencies();
3522
+ return JIPT.useJIPT ? "en-pt" : kaLocale;
3523
+ }
3524
+ function shouldUseLocalizedData() {
3525
+ return getLocale() !== "en";
3526
+ }
3527
+
3528
+ // A regex to split at the last / of a URL, separating the base part from the
3529
+ // hash. This is used to create the localized label data URLs.
3530
+ const splitHashRegex = /\/(?=[^/]+$)/;
3531
+ function getLocalizedDataUrl(url) {
3532
+ // For local (cached) graphie images, they are already localized.
3533
+ if (svgLocalLabelsRegex.test(url)) {
3534
+ return Util.getDataUrl(url);
3535
+ }
3536
+ const [base, hash] = Util.getBaseUrl(url).split(splitHashRegex);
3537
+ return `${base}/${getLocale()}/${hash}-data.json`;
3538
+ }
3539
+
3540
+ // Get the hash from the url, which is just the filename
3541
+ function getUrlHash(url) {
3542
+ const match = url.match(hashRegex);
3543
+ if (match == null) {
3544
+ throw new perseusCore.PerseusError("not a valid URL", perseusCore.Errors.InvalidInput);
3545
+ }
3546
+ return match && match[1];
3547
+ }
3548
+
3549
+ /**
3550
+ * Parse JSONP for a web+graphie://... hash to extract Graphie data, or throw
3551
+ * an error if invalid JSONP.
3552
+ */
3553
+ function parseDataFromJSONP(graphieHash, graphieJSONP, errorCallback) {
3554
+ // The JSONP is expected to be in the form of `svgDataHASH(...)` or `svgOtherDataHASH(...)`
3555
+ const match = graphieJSONP.match(new RegExp(`^(?:svgData|svgOtherData)${graphieHash}\\((.+)\\);$`));
3556
+
3557
+ // It is also possible that the JSONP is simply a JSON object,
3558
+ // in which case we can parse it directly.
3559
+ const jsonToParse = match ? match[1] : graphieJSONP;
3560
+
3561
+ // Try to parse the JSONP, and if it fails, call the error callback
3562
+ try {
3563
+ return JSON.parse(jsonToParse);
3564
+ } catch (error) {
3565
+ errorCallback(error);
3566
+ return null;
3567
+ }
3568
+ }
3569
+ // The global cache of label data. Its format is:
3570
+ // {
3571
+ // hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
3572
+ // loaded: a boolean of whether the data has been loaded or not
3573
+ // dataCallbacks: a list of callbacks to call with the data when the data
3574
+ // is loaded
3575
+ // data: the other data for this hash
3576
+ // },
3577
+ // ...
3578
+ // }
3579
+ const labelDataCache = {};
3580
+ function loadGraphie(url, onDataLoaded) {
3581
+ const hash = getUrlHash(url);
3582
+
3583
+ // We can't make multiple jsonp calls to the same file because their
3584
+ // callbacks will collide with each other. Instead, we cache the data
3585
+ // and only make the jsonp calls once.
3586
+ const entry = labelDataCache[hash];
3587
+ if (entry != null) {
3588
+ if (entry.loaded) {
3589
+ const {
3590
+ data,
3591
+ localized
3592
+ } = entry;
3593
+ onDataLoaded(data, localized);
3594
+ } else {
3595
+ entry.dataCallbacks.push(onDataLoaded);
3596
+ }
3597
+ } else {
3598
+ const cacheData = {
3599
+ loaded: false,
3600
+ dataCallbacks: [onDataLoaded],
3601
+ localized: shouldUseLocalizedData()
3602
+ };
3603
+ labelDataCache[hash] = cacheData;
3604
+ const retrieveData = async (url, errorCallback) => {
3605
+ const response = await fetch(url);
3606
+ if (!response?.ok) {
3607
+ errorCallback();
3608
+ return;
3609
+ }
3610
+ const jsonp = await response.text();
3611
+ const data = parseDataFromJSONP(hash, jsonp, error => {
3612
+ Log.error("Failed to parse JSONP for svg-image", perseusCore.Errors.Service, {
3613
+ cause: error,
3614
+ loggedMetadata: {
3615
+ dataUrl: Util.getDataUrl(url),
3616
+ jsonp
3617
+ }
3618
+ });
3619
+ });
3620
+
3621
+ // If the data is null, then we failed to parse the JSONP
3622
+ if (!data) {
3623
+ return;
3624
+ }
3625
+ const newCacheEntry = labelDataCache[hash] = {
3626
+ ...labelDataCache[hash],
3627
+ loaded: true,
3628
+ data
3629
+ };
3630
+ newCacheEntry.dataCallbacks.forEach(callback => {
3631
+ callback(newCacheEntry.data, cacheData.localized);
3632
+ });
3633
+ };
3634
+ const dataLoadErrorHandler = (x, status, error) => {
3635
+ Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
3636
+ cause: error,
3637
+ loggedMetadata: {
3638
+ dataUrl: Util.getDataUrl(url),
3639
+ status
3640
+ }
3641
+ });
3642
+ };
3643
+ if (shouldUseLocalizedData()) {
3644
+ cacheData.localized = false;
3645
+ // If there is isn't any localized data, fall back to
3646
+ // the original, unlocalized data
3647
+ retrieveData(getLocalizedDataUrl(url), dataLoadErrorHandler);
3648
+ } else {
3649
+ retrieveData(Util.getDataUrl(url), dataLoadErrorHandler);
3650
+ }
3651
+ }
3652
+ }
3653
+
3500
3654
  // Derived from the MIT-licensed:
3501
3655
 
3502
3656
  /*jshint browser:true, node:true */
@@ -3604,7 +3758,9 @@ function changeViewportTag(contentString, callback) {
3604
3758
  // finished resetting.
3605
3759
  // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
3606
3760
  // eslint-disable-next-line no-restricted-syntax
3607
- callback && setTimeout(callback, 0);
3761
+ if (callback != null) {
3762
+ setTimeout(callback, 0);
3763
+ }
3608
3764
  }, 0);
3609
3765
  }
3610
3766
 
@@ -8926,6 +9082,9 @@ class Graphie$1 {
8926
9082
  $__default["default"](el).css("position", "relative");
8927
9083
  this.raphael = Raphael$1(el);
8928
9084
 
9085
+ // Hide the Raphael canvas from screen readers
9086
+ $__default["default"](el).attr("aria-hidden", "true");
9087
+
8929
9088
  // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8930
9089
  $__default["default"](el).children("div").css("position", "absolute");
8931
9090
  }
@@ -16964,7 +17123,7 @@ class ImageLoader extends React__namespace.Component {
16964
17123
  onKeyUp = e => {
16965
17124
  // 13 is enter key, 32 is space key
16966
17125
  if (e.keyCode === 13 || e.keyCode === 32) {
16967
- imgProps.onClick && imgProps.onClick(e);
17126
+ imgProps.onClick?.(e);
16968
17127
  }
16969
17128
  };
16970
17129
  onKeyDown = e => {
@@ -17003,106 +17162,11 @@ class ImageLoader extends React__namespace.Component {
17003
17162
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
17004
17163
  // Minimum image width to make an image appear as zoomable.
17005
17164
  const ZOOMABLE_THRESHOLD = 700;
17006
-
17007
- // The global cache of label data. Its format is:
17008
- // {
17009
- // hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
17010
- // loaded: a boolean of whether the data has been loaded or not
17011
- // dataCallbacks: a list of callbacks to call with the data when the data
17012
- // is loaded
17013
- // data: the other data for this hash
17014
- // },
17015
- // ...
17016
- // }
17017
- const labelDataCache = {};
17018
-
17019
- // Write our own JSONP handler because all the other ones don't do things we
17020
- // need.
17021
- const doJSONP = function (url, options) {
17022
- options = {
17023
- callbackName: "callback",
17024
- success: $__default["default"].noop,
17025
- error: $__default["default"].noop,
17026
- ...options
17027
- };
17028
-
17029
- // Create the script
17030
- const script = document.createElement("script");
17031
- script.setAttribute("async", "");
17032
- script.setAttribute("src", url);
17033
-
17034
- // A cleanup function to run when we're done.
17035
- function cleanup() {
17036
- document.head && document.head.removeChild(script);
17037
- delete window[options.callbackName];
17038
- }
17039
-
17040
- // Add the global callback.
17041
- // @ts-expect-error - TS2740 - Type '() => void' is missing the following properties from type 'Window': clientInformation, closed, customElements, devicePixelRatio, and 206 more.
17042
- window[options.callbackName] = function () {
17043
- cleanup();
17044
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
17045
- args[_key] = arguments[_key];
17046
- }
17047
- options.success.apply(null, args);
17048
- };
17049
-
17050
- // Add the error handler.
17051
- script.addEventListener("error", function () {
17052
- cleanup();
17053
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
17054
- args[_key2] = arguments[_key2];
17055
- }
17056
- options.error.apply(null, args);
17057
- });
17058
-
17059
- // Insert the script to start the download.
17060
- document.head && document.head.appendChild(script);
17061
- };
17062
-
17063
- // For offline exercises in the mobile app, we download the graphie data
17064
- // (svgs and localized data files) and serve them from the local file
17065
- // system (with file://). We replace urls that start with `web+graphie`
17066
- // in the perseus json with this `file+graphie` prefix to indicate that
17067
- // they should have the `file://` protocol instead of `https://`.
17068
- const svgLocalLabelsRegex = /^file\+graphie:/;
17069
- const hashRegex = /\/([^/]+)$/;
17070
17165
  function isImageProbablyPhotograph(imageUrl) {
17071
17166
  // TODO(david): Do an inventory to refine this heuristic. For example, what
17072
17167
  // % of .png images are illustrations?
17073
17168
  return /\.(jpg|jpeg)$/i.test(imageUrl);
17074
17169
  }
17075
- function getLocale() {
17076
- const {
17077
- JIPT,
17078
- kaLocale
17079
- } = getDependencies();
17080
- return JIPT.useJIPT ? "en-pt" : kaLocale;
17081
- }
17082
- function shouldUseLocalizedData() {
17083
- return getLocale() !== "en";
17084
- }
17085
-
17086
- // A regex to split at the last / of a URL, separating the base part from the
17087
- // hash. This is used to create the localized label data URLs.
17088
- const splitHashRegex = /\/(?=[^/]+$)/;
17089
- function getLocalizedDataUrl(url) {
17090
- // For local (cached) graphie images, they are already localized.
17091
- if (svgLocalLabelsRegex.test(url)) {
17092
- return Util.getDataUrl(url);
17093
- }
17094
- const [base, hash] = Util.getBaseUrl(url).split(splitHashRegex);
17095
- return `${base}/${getLocale()}/${hash}-data.json`;
17096
- }
17097
-
17098
- // Get the hash from the url, which is just the filename
17099
- function getUrlHash(url) {
17100
- const match = url.match(hashRegex);
17101
- if (match == null) {
17102
- throw new perseusCore.PerseusError("not a valid URL", perseusCore.Errors.InvalidInput);
17103
- }
17104
- return match && match[1];
17105
- }
17106
17170
  function defaultPreloader(dimensions) {
17107
17171
  return /*#__PURE__*/React__namespace.createElement("span", {
17108
17172
  style: {
@@ -17189,90 +17253,22 @@ class SvgImage extends React__namespace.Component {
17189
17253
  return Util.isLabeledSVG(this.props.src) ? state.imageLoaded && state.dataLoaded : state.imageLoaded;
17190
17254
  }
17191
17255
  loadResources() {
17192
- const hash = getUrlHash(this.props.src);
17193
-
17194
- // We can't make multiple jsonp calls to the same file because their
17195
- // callbacks will collide with each other. Instead, we cache the data
17196
- // and only make the jsonp calls once.
17197
- if (labelDataCache[hash]) {
17198
- if (labelDataCache[hash].loaded) {
17199
- const {
17200
- data,
17201
- localized
17202
- } = labelDataCache[hash];
17203
- this.onDataLoaded(data, localized);
17204
- } else {
17205
- labelDataCache[hash].dataCallbacks.push(this.onDataLoaded);
17206
- }
17207
- } else {
17208
- const cacheData = {
17209
- loaded: false,
17210
- dataCallbacks: [this.onDataLoaded],
17211
- data: null,
17212
- localized: shouldUseLocalizedData()
17213
- };
17214
- labelDataCache[hash] = cacheData;
17215
- const retrieveData = (url, errorCallback) => {
17216
- doJSONP(url, {
17217
- callbackName: "svgData" + hash,
17218
- success: data => {
17219
- // @ts-expect-error - TS2540 - Cannot assign to 'data' because it is a read-only property.
17220
- cacheData.data = data;
17221
- // @ts-expect-error - TS2540 - Cannot assign to 'loaded' because it is a read-only property.
17222
- cacheData.loaded = true;
17223
- ___default["default"].each(cacheData.dataCallbacks, callback => {
17224
- // @ts-expect-error - TS2345 - Argument of type 'null' is not assignable to parameter of type '{ labels: readonly any[]; range: readonly any[]; }'.
17225
- callback(cacheData.data, cacheData.localized);
17226
- });
17227
- },
17228
- error: errorCallback
17229
- });
17230
- };
17231
- if (shouldUseLocalizedData()) {
17232
- retrieveData(getLocalizedDataUrl(this.props.src), (x, status, error) => {
17233
- // @ts-expect-error - TS2540 - Cannot assign to 'localized' because it is a read-only property.
17234
- cacheData.localized = false;
17235
-
17236
- // If there is isn't any localized data, fall back to
17237
- // the original, unlocalized data
17238
- retrieveData(Util.getDataUrl(this.props.src), (x, status, error) => {
17239
- Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
17240
- cause: error,
17241
- loggedMetadata: {
17242
- dataUrl: Util.getDataUrl(this.props.src),
17243
- status
17244
- }
17245
- });
17246
- });
17256
+ loadGraphie(this.props.src, (data, localized) => {
17257
+ if (this._isMounted && data.labels && data.range) {
17258
+ const labelsRendered = {};
17259
+ data.labels.forEach(label => {
17260
+ labelsRendered[label.content] = false;
17247
17261
  });
17248
- } else {
17249
- retrieveData(Util.getDataUrl(this.props.src), (x, status, error) => {
17250
- Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
17251
- cause: error,
17252
- loggedMetadata: {
17253
- dataUrl: Util.getDataUrl(this.props.src),
17254
- status
17255
- }
17256
- });
17262
+ this.setState({
17263
+ dataLoaded: true,
17264
+ labelDataIsLocalized: localized,
17265
+ labelsRendered,
17266
+ labels: data.labels,
17267
+ range: data.range
17257
17268
  });
17258
17269
  }
17259
- }
17270
+ });
17260
17271
  }
17261
- onDataLoaded = (data, localized) => {
17262
- if (this._isMounted && data.labels && data.range) {
17263
- const labelsRendered = data.labels.reduce((dict, label) => ({
17264
- ...dict,
17265
- [label.content]: false
17266
- }), {});
17267
- this.setState({
17268
- dataLoaded: true,
17269
- labelDataIsLocalized: localized,
17270
- labelsRendered,
17271
- labels: data.labels,
17272
- range: data.range
17273
- });
17274
- }
17275
- };
17276
17272
  sizeProvided() {
17277
17273
  return this.props.width != null && this.props.height != null;
17278
17274
  }
@@ -17396,7 +17392,7 @@ class SvgImage extends React__namespace.Component {
17396
17392
  $image.width() < this.props.width || this.props.zoomToFullSizeOnMobile) {
17397
17393
  ZoomService.handleZoomClick(e, this.props.zoomToFullSizeOnMobile);
17398
17394
  }
17399
- this.props.trackInteraction && this.props.trackInteraction();
17395
+ this.props.trackInteraction?.();
17400
17396
  };
17401
17397
  handleUpdate = status => {
17402
17398
  this.props.onUpdate();
@@ -19803,8 +19799,8 @@ class WidgetContainer extends React__namespace.Component {
19803
19799
  };
19804
19800
 
19805
19801
  // Note: if you add more props here, please consider whether or not
19806
- // it should be auto-serialized (e.g. used in scoreInput()). See
19807
- // widget-jsonify-deprecated.jsx and widget-prop-denylist.jsx
19802
+ // it should be auto-serialized.
19803
+ // See widget-jsonify-deprecated.jsx and widget-prop-denylist.jsx
19808
19804
 
19809
19805
  // We default to an empty object for style instead of null
19810
19806
  // because of a strange bug where the static styles aren't applied
@@ -21100,15 +21096,6 @@ class Renderer extends React__namespace.Component {
21100
21096
  return combinedScore;
21101
21097
  }
21102
21098
 
21103
- /**
21104
- * @deprecated use scorePerseusItem
21105
- */
21106
- guessAndScore = () => {
21107
- const totalGuess = this.getUserInput();
21108
- const totalScore = this.score();
21109
- return [totalGuess, totalScore];
21110
- };
21111
-
21112
21099
  // TranslationLinter callback
21113
21100
  handletranslationLintErrors = lintErrors => {
21114
21101
  if (!this._isMounted) {
@@ -21786,11 +21773,11 @@ class SimpleKeypadInput extends React__namespace.Component {
21786
21773
  keypadType: mathInput.KeypadType.FRACTION
21787
21774
  }, () => {
21788
21775
  if (_this._isMounted) {
21789
- onFocus && onFocus();
21776
+ onFocus?.();
21790
21777
  }
21791
21778
  });
21792
21779
  } else {
21793
- onFocus && onFocus();
21780
+ onFocus?.();
21794
21781
  }
21795
21782
  },
21796
21783
  value: value == null ? "" : "" + value
@@ -22029,9 +22016,6 @@ function scoreInputNumber(userInput, rubric, strings) {
22029
22016
  // If `currentValue` is not TeX, this should be a no-op.
22030
22017
  const currentValue = ParseTex$1(userInput.currentValue);
22031
22018
  const result = val(currentValue);
22032
-
22033
- // TODO(eater): Seems silly to translate result to this invalid/points
22034
- // thing and immediately translate it back in ItemRenderer.scoreInput()
22035
22019
  if (result.empty) {
22036
22020
  return {
22037
22021
  type: "invalid",
@@ -22395,10 +22379,6 @@ function scoreNumericInput(userInput, rubric, strings) {
22395
22379
  message: matchedAnswer?.message ?? null,
22396
22380
  guess: localValue
22397
22381
  };
22398
-
22399
- // TODO(eater): Seems silly to translate result to this
22400
- // invalid/points thing and immediately translate it
22401
- // back in ItemRenderer.scoreInput()
22402
22382
  if (result.empty) {
22403
22383
  return {
22404
22384
  type: "invalid",
@@ -25255,7 +25235,7 @@ const scrollElementToOffset = function (elementOrWindow, offset) {
25255
25235
  // TODO(jeff, WEB-1378): Use Wonder Blocks Timing API
25256
25236
  // eslint-disable-next-line no-restricted-syntax
25257
25237
  clearInterval(scrollInterval);
25258
- callback && callback();
25238
+ callback?.();
25259
25239
  }
25260
25240
  }, tickLength);
25261
25241
  };
@@ -27317,7 +27297,7 @@ class CSProgram extends React__namespace.Component {
27317
27297
  let data = {};
27318
27298
  try {
27319
27299
  data = JSON.parse(e.originalEvent.data);
27320
- } catch (err) {
27300
+ } catch {
27321
27301
  return;
27322
27302
  }
27323
27303
  if (___default["default"].isUndefined(data.testsPassed)) {
@@ -28344,7 +28324,6 @@ class GradedGroupSet extends React__namespace.Component {
28344
28324
  currentGroup: 0
28345
28325
  };
28346
28326
  shouldComponentUpdate(nextProps, nextState) {
28347
- nextProps.gradedGroups;
28348
28327
  return nextProps !== this.props || nextState !== this.state;
28349
28328
  }
28350
28329
  change = (() => {
@@ -29868,7 +29847,7 @@ class Iframe extends React__namespace.Component {
29868
29847
  let data = {};
29869
29848
  try {
29870
29849
  data = JSON.parse(e.originalEvent.data);
29871
- } catch (err) {
29850
+ } catch {
29872
29851
  return;
29873
29852
  }
29874
29853
  if (___default["default"].isUndefined(data.testsPassed)) {
@@ -30963,10 +30942,10 @@ class Graph extends React__namespace.Component {
30963
30942
  this.props.onMouseDown(coord);
30964
30943
  }, this) : undefined;
30965
30944
  const onMouseOver = $instructionsWrapper ? function () {
30966
- $instructionsWrapper && $instructionsWrapper.css("opacity", invisible);
30945
+ $instructionsWrapper?.css("opacity", invisible);
30967
30946
  } : undefined;
30968
30947
  const onMouseOut = $instructionsWrapper ? function () {
30969
- $instructionsWrapper && $instructionsWrapper.css("opacity", visible);
30948
+ $instructionsWrapper?.css("opacity", visible);
30970
30949
  } : undefined;
30971
30950
  /* eslint-enable indent */
30972
30951
 
@@ -31679,6 +31658,7 @@ function replaceOutsideTeX(mathString) {
31679
31658
  const parsed = mathOnlyParser(mathString);
31680
31659
  let result = "";
31681
31660
  for (const piece of parsed) {
31661
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
31682
31662
  piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
31683
31663
  }
31684
31664
  return `\\text{${result}}`;
@@ -34650,42 +34630,64 @@ function renderPointGraph(state, dispatch) {
34650
34630
  };
34651
34631
  }
34652
34632
  function PointGraph(props) {
34653
- const numPoints = props.graphState.numPoints;
34633
+ const {
34634
+ numPoints
34635
+ } = props.graphState;
34636
+ const graphConfig = useGraphConfig();
34637
+ const pointsRef = React__namespace.useRef([]);
34638
+
34639
+ // Dimensions to build the graph overlay for Unlimited Point.
34640
+ const {
34641
+ range: [x, y]
34642
+ } = graphConfig;
34643
+ const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
34644
+
34645
+ // This useEffect is to handle the focus snapping for Unlimited Point.
34646
+ React__namespace.useEffect(() => {
34647
+ const focusedIndex = props.graphState.focusedPointIndex;
34648
+ if (focusedIndex != null) {
34649
+ pointsRef.current[focusedIndex]?.focus();
34650
+ }
34651
+ }, [props.graphState.focusedPointIndex, pointsRef]);
34652
+ const statefulProps = {
34653
+ ...props,
34654
+ graphConfig,
34655
+ pointsRef,
34656
+ top,
34657
+ left
34658
+ };
34654
34659
  if (numPoints === "unlimited") {
34655
- return UnlimitedPointGraph(props);
34660
+ return UnlimitedPointGraph(statefulProps);
34656
34661
  }
34657
- return LimitedPointGraph(props);
34662
+ return LimitedPointGraph(statefulProps);
34658
34663
  }
34659
- function LimitedPointGraph(props) {
34664
+ function LimitedPointGraph(statefulProps) {
34660
34665
  const {
34661
34666
  dispatch
34662
- } = props;
34663
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34667
+ } = statefulProps;
34668
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, statefulProps.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34664
34669
  key: i,
34665
34670
  point: point,
34666
34671
  sequenceNumber: i + 1,
34667
34672
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34668
34673
  })));
34669
34674
  }
34670
- function UnlimitedPointGraph(props) {
34675
+ function UnlimitedPointGraph(statefulProps) {
34671
34676
  const {
34672
- dispatch
34673
- } = props;
34674
- const graphConfig = useGraphConfig();
34677
+ dispatch,
34678
+ graphConfig,
34679
+ pointsRef,
34680
+ top,
34681
+ left
34682
+ } = statefulProps;
34683
+ const {
34684
+ coords
34685
+ } = statefulProps.graphState;
34675
34686
  const {
34676
- range: [[minX, maxX], [minY, maxY]]
34687
+ graphDimensionsInPixels
34677
34688
  } = graphConfig;
34678
- const width = maxX - minX;
34679
- const height = maxY - minY;
34680
- const [[widthPx, heightPx]] = useTransformDimensionsToPixels([width, height]);
34681
- const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
34682
- const itemsRef = React__namespace.useRef([]);
34683
- React__namespace.useEffect(() => {
34684
- const focusedIndex = props.graphState.focusedPointIndex;
34685
- if (focusedIndex != null) {
34686
- itemsRef.current[focusedIndex]?.focus();
34687
- }
34688
- }, [props.graphState.focusedPointIndex, itemsRef]);
34689
+ const widthPx = graphDimensionsInPixels[0];
34690
+ const heightPx = graphDimensionsInPixels[1];
34689
34691
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34690
34692
  style: {
34691
34693
  fill: "rgba(0,0,0,0)",
@@ -34702,13 +34704,13 @@ function UnlimitedPointGraph(props) {
34702
34704
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34703
34705
  dispatch(actions.pointGraph.addPoint(graphCoordinates[0]));
34704
34706
  }
34705
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34707
+ }), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34706
34708
  key: i,
34707
34709
  point: point,
34708
34710
  sequenceNumber: i + 1,
34709
34711
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34710
34712
  ref: ref => {
34711
- itemsRef.current[i] = ref;
34713
+ pointsRef.current[i] = ref;
34712
34714
  },
34713
34715
  onFocus: () => {
34714
34716
  dispatch(actions.pointGraph.focusPoint(i));
@@ -34759,36 +34761,42 @@ function renderPolygonGraph(state, dispatch) {
34759
34761
  interactiveElementsDescription: null
34760
34762
  };
34761
34763
  }
34762
- const LimitedPolygonGraph = props => {
34763
- const [hovered, setHovered] = React__namespace.useState(false);
34764
- // This is more so required for the re-rendering that occurs when state
34765
- // updates; specifically with regard to line weighting and polygon focus.
34766
- const [focusVisible, setFocusVisible] = React__namespace.useState(false);
34764
+ const PolygonGraph = props => {
34767
34765
  const {
34768
34766
  dispatch
34769
34767
  } = props;
34770
34768
  const {
34769
+ numSides,
34771
34770
  coords,
34772
- showAngles,
34773
- showSides,
34774
- range,
34775
34771
  snapStep,
34776
34772
  snapTo = "grid"
34777
34773
  } = props.graphState;
34774
+ const graphConfig = useGraphConfig();
34775
+
34776
+ // Ref to implement the dragging behavior on a Limited/Closed Polygon.
34777
+ const polygonRef = React__namespace.useRef(null);
34778
+ // Ref to manage dynamic focus for the points in Unlimited Polygon
34779
+ const pointsRef = React__namespace.useRef([]);
34780
+ // Ref to manage the last move time for a Limited Polygon.
34781
+ const lastMoveTimeRef = React__namespace.useRef(0);
34782
+
34783
+ // Dimensions to build the graph overlay for Unlimited Polgyon.
34778
34784
  const {
34779
- disableKeyboardInteraction
34780
- } = useGraphConfig();
34785
+ range: [x, y]
34786
+ } = graphConfig;
34787
+ const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
34781
34788
 
34782
34789
  // TODO(benchristel): can the default set of points be removed here? I don't
34783
34790
  // think coords can be null.
34784
34791
  const points = coords ?? [[0, 0]];
34785
- const ref = React__namespace.useRef(null);
34792
+
34793
+ // Logic to build the dragging experience. Primarily used by Limited Polygon.
34786
34794
  const dragReferencePoint = points[0];
34787
34795
  const constrain = ["angles", "sides"].includes(snapTo) ? p => p : p => snap(snapStep, p);
34788
34796
  const {
34789
34797
  dragging
34790
34798
  } = useDraggable({
34791
- gestureTarget: ref,
34799
+ gestureTarget: polygonRef,
34792
34800
  point: dragReferencePoint,
34793
34801
  onMove: newPoint => {
34794
34802
  const delta = mafs.vec.sub(newPoint, dragReferencePoint);
@@ -34796,7 +34804,61 @@ const LimitedPolygonGraph = props => {
34796
34804
  },
34797
34805
  constrainKeyboardMovement: constrain
34798
34806
  });
34799
- const lastMoveTime = React__namespace.useRef(0);
34807
+
34808
+ // Hover/focus state for Limited Polgyon effects.
34809
+ const [hovered, setHovered] = React__namespace.useState(false);
34810
+ // This is more so required for the re-rendering that occurs when state
34811
+ // updates; specifically with regard to line weighting and polygon focus.
34812
+ const [focusVisible, setFocusVisible] = React__namespace.useState(false);
34813
+
34814
+ // This useEffect is to handle the focus snapping for Unlimited Polygon.
34815
+ React__namespace.useEffect(() => {
34816
+ const focusedIndex = props.graphState.focusedPointIndex;
34817
+ if (focusedIndex != null) {
34818
+ pointsRef.current[focusedIndex]?.focus();
34819
+ }
34820
+ }, [props.graphState.focusedPointIndex, pointsRef]);
34821
+ const statefulProps = {
34822
+ ...props,
34823
+ graphConfig,
34824
+ polygonRef,
34825
+ pointsRef,
34826
+ lastMoveTimeRef,
34827
+ left,
34828
+ top,
34829
+ dragging,
34830
+ points,
34831
+ constrain,
34832
+ hovered,
34833
+ setHovered,
34834
+ focusVisible,
34835
+ setFocusVisible
34836
+ };
34837
+ return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
34838
+ };
34839
+ const LimitedPolygonGraph = statefulProps => {
34840
+ const {
34841
+ dispatch,
34842
+ hovered,
34843
+ setHovered,
34844
+ focusVisible,
34845
+ setFocusVisible,
34846
+ graphConfig,
34847
+ polygonRef,
34848
+ lastMoveTimeRef,
34849
+ dragging,
34850
+ points,
34851
+ constrain
34852
+ } = statefulProps;
34853
+ const {
34854
+ showAngles,
34855
+ showSides,
34856
+ range,
34857
+ snapTo = "grid"
34858
+ } = statefulProps.graphState;
34859
+ const {
34860
+ disableKeyboardInteraction
34861
+ } = graphConfig;
34800
34862
  const lines = getLines(points);
34801
34863
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
34802
34864
  points: [...points],
@@ -34835,7 +34897,7 @@ const LimitedPolygonGraph = props => {
34835
34897
  points: [...points],
34836
34898
  color: "transparent",
34837
34899
  svgPolygonProps: {
34838
- ref,
34900
+ ref: polygonRef,
34839
34901
  tabIndex: disableKeyboardInteraction ? -1 : 0,
34840
34902
  strokeWidth: TARGET_SIZE,
34841
34903
  style: {
@@ -34847,13 +34909,13 @@ const LimitedPolygonGraph = props => {
34847
34909
  // Required to remove line weighting when user clicks away
34848
34910
  // from the focused polygon
34849
34911
  onKeyDownCapture: () => {
34850
- setFocusVisible(hasFocusVisible(ref.current));
34912
+ setFocusVisible(hasFocusVisible(polygonRef.current));
34851
34913
  },
34852
34914
  // Required for lines to darken on focus
34853
- onFocus: () => setFocusVisible(hasFocusVisible(ref.current)),
34915
+ onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
34854
34916
  // Required for line weighting to update on blur. Without this,
34855
34917
  // the user has to hover over the shape for it to update
34856
- onBlur: () => setFocusVisible(hasFocusVisible(ref.current)),
34918
+ onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
34857
34919
  className: "movable-polygon"
34858
34920
  }
34859
34921
  }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
@@ -34865,94 +34927,86 @@ const LimitedPolygonGraph = props => {
34865
34927
  const now = Date.now();
34866
34928
  const targetFPS = 40;
34867
34929
  const moveThresholdTime = 1000 / targetFPS;
34868
- if (now - lastMoveTime.current > moveThresholdTime) {
34930
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
34869
34931
  dispatch(actions.polygon.movePoint(i, destination));
34870
- lastMoveTime.current = now;
34932
+ lastMoveTimeRef.current = now;
34871
34933
  }
34872
34934
  }
34873
34935
  })));
34874
34936
  };
34875
- const UnlimitedPolygonGraph = props => {
34937
+ const UnlimitedPolygonGraph = statefulProps => {
34876
34938
  const {
34877
- dispatch
34878
- } = props;
34939
+ dispatch,
34940
+ graphConfig,
34941
+ left,
34942
+ top,
34943
+ pointsRef,
34944
+ points
34945
+ } = statefulProps;
34879
34946
  const {
34880
34947
  coords,
34881
34948
  closedPolygon
34882
- } = props.graphState;
34883
- const graphConfig = useGraphConfig();
34884
- const {
34885
- range: [x, y],
34886
- graphDimensionsInPixels
34887
- } = graphConfig;
34888
- const widthPx = graphDimensionsInPixels[0];
34889
- const heightPx = graphDimensionsInPixels[1];
34890
- const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
34891
- const pointRef = React__namespace.useRef([]);
34949
+ } = statefulProps.graphState;
34892
34950
 
34893
- // TODO(benchristel): can the default set of points be removed here? I don't
34894
- // think coords can be null.
34895
- const points = coords ?? [[0, 0]];
34896
- React__namespace.useEffect(() => {
34897
- const focusedIndex = props.graphState.focusedPointIndex;
34898
- if (focusedIndex != null) {
34899
- pointRef.current[focusedIndex]?.focus();
34900
- }
34901
- }, [props.graphState.focusedPointIndex, pointRef]);
34951
+ // If the polygon is closed, return a LimitedPolygon component.
34902
34952
  if (closedPolygon) {
34903
34953
  const closedPolygonProps = {
34904
- ...props,
34954
+ ...statefulProps,
34905
34955
  numSides: coords.length
34906
34956
  };
34907
34957
  return /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, closedPolygonProps);
34908
- } else {
34909
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34958
+ }
34959
+ const {
34960
+ graphDimensionsInPixels
34961
+ } = graphConfig;
34962
+ const widthPx = graphDimensionsInPixels[0];
34963
+ const heightPx = graphDimensionsInPixels[1];
34964
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34965
+ style: {
34966
+ fill: "rgba(0,0,0,0)",
34967
+ cursor: "crosshair"
34968
+ },
34969
+ width: widthPx,
34970
+ height: heightPx,
34971
+ x: left,
34972
+ y: top,
34973
+ onClick: event => {
34974
+ const elementRect = event.currentTarget.getBoundingClientRect();
34975
+ const x = event.clientX - elementRect.x;
34976
+ const y = event.clientY - elementRect.y;
34977
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34978
+ dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34979
+ }
34980
+ }), /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
34981
+ points: [...points],
34982
+ color: "var(--movable-line-stroke-color)",
34983
+ svgPolylineProps: {
34984
+ strokeWidth: "var(--movable-line-stroke-weight)",
34910
34985
  style: {
34911
- fill: "rgba(0,0,0,0)",
34912
- cursor: "crosshair"
34913
- },
34914
- width: widthPx,
34915
- height: heightPx,
34916
- x: left,
34917
- y: top,
34918
- onClick: event => {
34919
- const elementRect = event.currentTarget.getBoundingClientRect();
34920
- const x = event.clientX - elementRect.x;
34921
- const y = event.clientY - elementRect.y;
34922
- const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34923
- dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34924
- }
34925
- }), /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
34926
- points: [...points],
34927
- color: "var(--movable-line-stroke-color)",
34928
- svgPolylineProps: {
34929
- strokeWidth: "var(--movable-line-stroke-weight)",
34930
- style: {
34931
- fill: "transparent"
34932
- }
34986
+ fill: "transparent"
34933
34987
  }
34934
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34935
- key: i,
34936
- point: point,
34937
- sequenceNumber: i + 1,
34938
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34939
- ref: ref => {
34940
- pointRef.current[i] = ref;
34941
- },
34942
- onFocus: () => {
34943
- dispatch(actions.polygon.focusPoint(i));
34944
- },
34945
- onClick: () => {
34946
- // If the point being clicked is the first point and
34947
- // there's enough points to form a polygon (3 or more)
34948
- // Close the shape before setting focus.
34949
- if (i === 0 && props.graphState.coords.length >= 3) {
34950
- dispatch(actions.polygon.closePolygon());
34951
- }
34952
- dispatch(actions.polygon.clickPoint(i));
34988
+ }
34989
+ }), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34990
+ key: i,
34991
+ point: point,
34992
+ sequenceNumber: i + 1,
34993
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34994
+ ref: ref => {
34995
+ pointsRef.current[i] = ref;
34996
+ },
34997
+ onFocus: () => {
34998
+ dispatch(actions.polygon.focusPoint(i));
34999
+ },
35000
+ onClick: () => {
35001
+ // If the point being clicked is the first point and
35002
+ // there's enough points to form a polygon (3 or more)
35003
+ // Close the shape before setting focus.
35004
+ if (i === 0 && coords.length >= 3) {
35005
+ dispatch(actions.polygon.closePolygon());
34953
35006
  }
34954
- })));
34955
- }
35007
+ dispatch(actions.polygon.clickPoint(i));
35008
+ }
35009
+ })));
34956
35010
  };
34957
35011
  function getLines(points) {
34958
35012
  return points.map((point, i) => {
@@ -34964,17 +35018,13 @@ const hasFocusVisible = element => {
34964
35018
  const matches = selector => element?.matches(selector) ?? false;
34965
35019
  try {
34966
35020
  return matches(":focus-visible");
34967
- } catch (e) {
35021
+ } catch {
34968
35022
  // jsdom doesn't support :focus-visible
34969
35023
  // (see https://github.com/jsdom/jsdom/issues/3426),
34970
35024
  // so the call to matches(":focus-visible") will fail in tests.
34971
35025
  return matches(":focus");
34972
35026
  }
34973
35027
  };
34974
- const PolygonGraph = props => {
34975
- const numSides = props.graphState.numSides;
34976
- return numSides === "unlimited" ? UnlimitedPolygonGraph(props) : LimitedPolygonGraph(props);
34977
- };
34978
35028
 
34979
35029
  function renderQuadraticGraph(state, dispatch) {
34980
35030
  return {
@@ -40395,7 +40445,7 @@ class Sortable extends React__namespace.Component {
40395
40445
  this.setState({
40396
40446
  items
40397
40447
  }, () => {
40398
- this.props.onMeasure && this.props.onMeasure({
40448
+ this.props.onMeasure?.({
40399
40449
  widths: widths,
40400
40450
  heights: heights
40401
40451
  });
@@ -40445,7 +40495,7 @@ class Sortable extends React__namespace.Component {
40445
40495
  this.setState({
40446
40496
  items: nextItems
40447
40497
  });
40448
- this.props.onChange && this.props.onChange({});
40498
+ this.props.onChange?.({});
40449
40499
  }
40450
40500
  onMouseMove(key) {
40451
40501
  // Dragging: Rearrange items based on draggable's position
@@ -40525,7 +40575,7 @@ class Sortable extends React__namespace.Component {
40525
40575
  // HACK: We need to know *that* the widget changed, but currently it's
40526
40576
  // not set up in a nice way to tell us *how* it changed, since the
40527
40577
  // permutation of the items is stored in state.
40528
- this.props.onChange && this.props.onChange({});
40578
+ this.props.onChange?.({});
40529
40579
  });
40530
40580
 
40531
40581
  // @ts-expect-error - TS2339 - Property 'animationFrameRequest' does not exist on type 'Sortable'.
@@ -43250,7 +43300,7 @@ class Card extends React__namespace.Component {
43250
43300
  dragging: true
43251
43301
  });
43252
43302
  this.bindMouseMoveUp();
43253
- this.props.onMouseDown && this.props.onMouseDown(loc, this);
43303
+ this.props.onMouseDown?.(loc, this);
43254
43304
  }
43255
43305
  };
43256
43306
  onMouseMove = event => {
@@ -43260,7 +43310,7 @@ class Card extends React__namespace.Component {
43260
43310
  event.preventDefault();
43261
43311
  const loc = Util.extractPointerLocation(event);
43262
43312
  if (loc) {
43263
- this.props.onMouseMove && this.props.onMouseMove(loc);
43313
+ this.props.onMouseMove?.(loc);
43264
43314
  }
43265
43315
  };
43266
43316
  onMouseUp = event => {
@@ -43271,7 +43321,7 @@ class Card extends React__namespace.Component {
43271
43321
  dragging: false
43272
43322
  });
43273
43323
  this.unbindMouseMoveUp();
43274
- this.props.onMouseUp && this.props.onMouseUp(loc);
43324
+ this.props.onMouseUp?.(loc);
43275
43325
  }
43276
43326
  };
43277
43327
  render() {
@@ -45477,7 +45527,7 @@ const init = function () {
45477
45527
 
45478
45528
  // This file is processed by a Rollup plugin (replace) to inject the production
45479
45529
  const libName = "@khanacademy/perseus";
45480
- const libVersion = "47.0.1";
45530
+ const libVersion = "48.1.0";
45481
45531
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45482
45532
 
45483
45533
  const apiVersion = {
@@ -46237,28 +46287,6 @@ class ServerItemRenderer extends React__namespace.Component {
46237
46287
  return this.questionRenderer.getUserInputMap();
46238
46288
  }
46239
46289
 
46240
- /**
46241
- * Grades the item.
46242
- *
46243
- * @deprecated use scorePerseusItem
46244
- */
46245
- scoreInput() {
46246
- const guess = this.getUserInput();
46247
- const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
46248
-
46249
- // Continue to include an empty guess for the now defunct answer area.
46250
- // TODO(alex): Check whether we rely on the format here for
46251
- // analyzing ProblemLogs. If not, remove this layer.
46252
- const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
46253
- const keScore = keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46254
- const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46255
- this.setState({
46256
- questionCompleted: keScore.correct,
46257
- questionHighlightedWidgets: emptyQuestionAreaWidgets
46258
- });
46259
- return keScore;
46260
- }
46261
-
46262
46290
  /**
46263
46291
  * Returns an array of all widget IDs in the order they occur in
46264
46292
  * the question content.
@@ -49165,6 +49193,7 @@ exports.lockedFigureFillStyles = lockedFigureFillStyles;
49165
49193
  exports.makeSafeUrl = makeSafeUrl;
49166
49194
  exports.mathOnlyParser = mathOnlyParser;
49167
49195
  exports.mediaQueries = mediaQueries;
49196
+ exports.parseDataFromJSONP = parseDataFromJSONP;
49168
49197
  exports.parsePerseusItem = parsePerseusItem;
49169
49198
  exports.plotterPlotTypes = plotterPlotTypes;
49170
49199
  exports.preprocessTex = preprocessTex;