@khanacademy/perseus 20.8.0 → 21.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
@@ -2,7 +2,7 @@ import * as KAS from '@khanacademy/kas';
2
2
  import { keyTranslator, CursorContext, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
3
  import * as PerseusLinter from '@khanacademy/perseus-linter';
4
4
  import { linterContextDefault, Rule, linterContextProps } from '@khanacademy/perseus-linter';
5
- import { View, RenderStateRoot, UniqueIDProvider, useUniqueIdWithMock, Text as Text$1 } from '@khanacademy/wonder-blocks-core';
5
+ import { View, RenderStateRoot, UniqueIDProvider, useUniqueIdWithMock, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
6
6
  import * as i18n from '@khanacademy/wonder-blocks-i18n';
7
7
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
8
8
  import classNames from 'classnames';
@@ -10,9 +10,8 @@ import * as React from 'react';
10
10
  import React__default, { useState, useEffect, useRef } from 'react';
11
11
  import _, { debounce as debounce$1 } from 'underscore';
12
12
  import Clickable from '@khanacademy/wonder-blocks-clickable';
13
- import Color, { fade } from '@khanacademy/wonder-blocks-color';
14
13
  import { Popover, PopoverContentCore, PopoverContent } from '@khanacademy/wonder-blocks-popover';
15
- import Spacing from '@khanacademy/wonder-blocks-spacing';
14
+ import { color, spacing } from '@khanacademy/wonder-blocks-tokens';
16
15
  import { StyleSheet, css } from 'aphrodite';
17
16
  import $ from 'jquery';
18
17
  import PropTypes from 'prop-types';
@@ -31,8 +30,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
31
30
  import caretDown from '@phosphor-icons/core/assets/regular/caret-down.svg';
32
31
  import caretUp from '@phosphor-icons/core/assets/regular/caret-up.svg';
33
32
  import debounce$2 from 'lodash.debounce';
34
- import { Point as Point$2, vec, useTransformContext, useMovable, Vector, Polygon, Coordinates, Mafs } from 'mafs';
35
- import { color } from '@khanacademy/wonder-blocks-tokens';
33
+ import { Point as Point$2, useTransformContext, vec, useMovable, Vector, Text as Text$1, Polygon, Coordinates, Mafs } from 'mafs';
36
34
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
37
35
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
38
36
  import { LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
@@ -331,16 +329,16 @@ const MathInputIcon = ({
331
329
  focused,
332
330
  active
333
331
  }) => {
334
- let color;
332
+ let fillColor;
335
333
  switch (true) {
336
334
  case focused || active:
337
- color = Color.white;
335
+ fillColor = color.white;
338
336
  break;
339
337
  case hovered:
340
- color = Color.blue;
338
+ fillColor = color.blue;
341
339
  break;
342
340
  default:
343
- color = Color.offBlack;
341
+ fillColor = color.offBlack;
344
342
  break;
345
343
  }
346
344
  const dynamicClass = active || focused ? styles$D.iconActive : styles$D.iconInactive;
@@ -350,7 +348,7 @@ const MathInputIcon = ({
350
348
  xmlns: "http://www.w3.org/2000/svg",
351
349
  width: "16",
352
350
  height: "16",
353
- fill: color,
351
+ fill: fillColor,
354
352
  viewBox: "0 0 256 256"
355
353
  }, /*#__PURE__*/React.createElement("path", {
356
354
  d: "M112,72a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16h64A8,8,0,0,1,112,72Zm-8,104H80V152a8,8,0,0,0-16,0v24H40a8,8,0,0,0,0,16H64v24a8,8,0,0,0,16,0V192h24a8,8,0,0,0,0-16Zm48,0h64a8,8,0,0,0,0-16H152a8,8,0,0,0,0,16Zm64,16H152a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16Zm-61.66-90.34a8,8,0,0,0,11.32,0L184,83.31l18.34,18.35a8,8,0,0,0,11.32-11.32L195.31,72l18.35-18.34a8,8,0,0,0-11.32-11.32L184,60.69,165.66,42.34a8,8,0,0,0-11.32,11.32L172.69,72,154.34,90.34A8,8,0,0,0,154.34,101.66Z"
@@ -387,7 +385,7 @@ const mapButtonSets = buttonSets => {
387
385
  };
388
386
  const inputFocused = {
389
387
  borderWidth: 2,
390
- borderColor: Color.blue,
388
+ borderColor: color.blue,
391
389
  margin: -1
392
390
  };
393
391
  const styles$D = StyleSheet.create({
@@ -395,35 +393,35 @@ const styles$D = StyleSheet.create({
395
393
  display: "flex",
396
394
  justifyContent: "center",
397
395
  height: "100%",
398
- padding: Spacing.xxxSmall_4,
396
+ padding: spacing.xxxSmall_4,
399
397
  borderRadius: 1
400
398
  },
401
399
  iconInactive: {
402
- backgroundColor: Color.offBlack8
400
+ backgroundColor: color.offBlack8
403
401
  },
404
402
  iconActive: {
405
- backgroundColor: Color.offBlack64
403
+ backgroundColor: color.offBlack64
406
404
  },
407
405
  outerWrapper: {
408
406
  display: "inline-block",
409
407
  borderStyle: "solid",
410
408
  borderWidth: 1,
411
- borderColor: Color.offBlack50,
409
+ borderColor: color.offBlack50,
412
410
  borderRadius: 3,
413
- background: Color.white,
411
+ background: color.white,
414
412
  ":hover": inputFocused
415
413
  },
416
414
  wrapperFocused: inputFocused,
417
415
  wrapperError: {
418
- borderColor: Color.red,
419
- background: fade(Color.red, 0.08),
416
+ borderColor: color.red,
417
+ background: color.fadedRed8,
420
418
  ":hover": {
421
- borderColor: Color.red
419
+ borderColor: color.red
422
420
  }
423
421
  },
424
422
  popoverContent: {
425
423
  padding: 0,
426
- paddingBottom: Spacing.xxSmall_6,
424
+ paddingBottom: spacing.xxSmall_6,
427
425
  maxWidth: "initial"
428
426
  }
429
427
  });
@@ -1923,7 +1921,7 @@ const KhanAnswerTypes = {
1923
1921
  }
1924
1922
  };
1925
1923
 
1926
- const _excluded$4 = ["scoring"];
1924
+ const _excluded$5 = ["scoring"];
1927
1925
  const ERROR_TITLE = i18n._("Oops!");
1928
1926
  const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
1929
1927
 
@@ -2011,7 +2009,7 @@ class Expression extends React.Component {
2011
2009
  let {
2012
2010
  scoring
2013
2011
  } = _ref,
2014
- rubric = _objectWithoutPropertiesLoose(_ref, _excluded$4);
2012
+ rubric = _objectWithoutPropertiesLoose(_ref, _excluded$5);
2015
2013
  const score = Expression.validate(this.getUserInput(), rubric, onInputError || function () {});
2016
2014
 
2017
2015
  // "scoring" is a flag that indicates when we are checking answers.
@@ -18834,7 +18832,7 @@ const isAccessible = widgetInfo => {
18834
18832
  }
18835
18833
  return !!accessible;
18836
18834
  };
18837
- const getAllWidgetTypes$1 = () => {
18835
+ const getAllWidgetTypes = () => {
18838
18836
  return _.keys(widgets);
18839
18837
  };
18840
18838
  const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
@@ -19103,7 +19101,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19103
19101
  getVersionVector: getVersionVector,
19104
19102
  getPublicWidgets: getPublicWidgets,
19105
19103
  isAccessible: isAccessible,
19106
- getAllWidgetTypes: getAllWidgetTypes$1,
19104
+ getAllWidgetTypes: getAllWidgetTypes,
19107
19105
  upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
19108
19106
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
19109
19107
  traverseChildWidgets: traverseChildWidgets$3,
@@ -19341,6 +19339,9 @@ class Renderer extends React.Component {
19341
19339
  if (!widgetInfo.type || !widgetInfo.alignment) {
19342
19340
  const newValues = {};
19343
19341
  if (!widgetInfo.type) {
19342
+ // TODO: why does widget have no type?
19343
+ // We don't want to derive type from widget ID
19344
+ // see: LEMS-1845
19344
19345
  newValues.type = widgetId.split(" ")[0];
19345
19346
  }
19346
19347
  if (!widgetInfo.alignment) {
@@ -21430,7 +21431,7 @@ InputWithExamples.defaultProps = {
21430
21431
  className: ""
21431
21432
  };
21432
21433
 
21433
- const _excluded$3 = ["keypadElement", "onFocus", "value"];
21434
+ const _excluded$4 = ["keypadElement", "onFocus", "value"];
21434
21435
  class SimpleKeypadInput extends React.Component {
21435
21436
  constructor(...args) {
21436
21437
  super(...args);
@@ -21469,7 +21470,7 @@ class SimpleKeypadInput extends React.Component {
21469
21470
  onFocus,
21470
21471
  value
21471
21472
  } = _this$props,
21472
- rest = _objectWithoutPropertiesLoose(_this$props, _excluded$3);
21473
+ rest = _objectWithoutPropertiesLoose(_this$props, _excluded$4);
21473
21474
  return (
21474
21475
  /*#__PURE__*/
21475
21476
  // @ts-expect-error - TS2769 - No overload matches this call.
@@ -21763,13 +21764,13 @@ const styles$A = StyleSheet.create({
21763
21764
  width: 40
21764
21765
  },
21765
21766
  leftAlign: {
21766
- paddingLeft: Spacing.xxxSmall_4,
21767
+ paddingLeft: spacing.xxxSmall_4,
21767
21768
  paddingRight: 0
21768
21769
  },
21769
21770
  rightAlign: {
21770
21771
  textAlign: "right",
21771
21772
  paddingLeft: 0,
21772
- paddingRight: Spacing.xxxSmall_4
21773
+ paddingRight: spacing.xxxSmall_4
21773
21774
  },
21774
21775
  answerStateUnanswered: {
21775
21776
  backgroundColor: "#eee",
@@ -22867,19 +22868,19 @@ function ChoiceInner(props) {
22867
22868
  function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct) {
22868
22869
  let backgroundColor;
22869
22870
  let borderColor;
22870
- let color;
22871
+ let color$1;
22871
22872
  if (!showCorrectness && pressed) {
22872
- borderColor = Color.blue;
22873
- color = Color.blue;
22873
+ borderColor = color.blue;
22874
+ color$1 = color.blue;
22874
22875
  backgroundColor = "transparent";
22875
22876
  } else if (checked) {
22876
- const bg = showCorrectness && correct ? Color.green : Color.blue;
22877
- color = white$1;
22877
+ const bg = showCorrectness && correct ? color.green : color.blue;
22878
+ color$1 = white$1;
22878
22879
  backgroundColor = bg;
22879
22880
  borderColor = bg;
22880
22881
  } else {
22881
- borderColor = Color.offBlack64;
22882
- color = Color.offBlack64;
22882
+ borderColor = color.offBlack64;
22883
+ color$1 = color.offBlack64;
22883
22884
  }
22884
22885
 
22885
22886
  // define shape
@@ -22892,7 +22893,7 @@ function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, cor
22892
22893
  return {
22893
22894
  backgroundColor,
22894
22895
  borderColor,
22895
- color,
22896
+ color: color$1,
22896
22897
  borderRadius
22897
22898
  };
22898
22899
  }
@@ -22913,7 +22914,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
22913
22914
  return /*#__PURE__*/React.createElement("div", {
22914
22915
  className: css(styles$y.iconWrapper)
22915
22916
  }, /*#__PURE__*/React.createElement(FocusRing, {
22916
- color: Color.blue,
22917
+ color: color.blue,
22917
22918
  visible: focused || hovered,
22918
22919
  multipleSelect: multipleSelect
22919
22920
  }, /*#__PURE__*/React.createElement("div", {
@@ -22978,9 +22979,9 @@ const styles$w = StyleSheet.create({
22978
22979
  color: gray68
22979
22980
  },
22980
22981
  circleIncorrectAnswered: {
22981
- backgroundColor: Color.red,
22982
- borderColor: Color.red,
22983
- color: Color.white
22982
+ backgroundColor: color.red,
22983
+ borderColor: color.red,
22984
+ color: color.white
22984
22985
  }
22985
22986
  });
22986
22987
 
@@ -23048,13 +23049,13 @@ const styles$v = StyleSheet.create({
23048
23049
  textTransform: "uppercase"
23049
23050
  },
23050
23051
  correct: {
23051
- color: Color.green
23052
+ color: color.green
23052
23053
  },
23053
23054
  incorrectAnswered: {
23054
- color: Color.red
23055
+ color: color.red
23055
23056
  },
23056
23057
  incorrect: {
23057
- color: Color.offBlack64
23058
+ color: color.offBlack64
23058
23059
  }
23059
23060
  });
23060
23061
 
@@ -23129,7 +23130,7 @@ const Choice = function Choice(props) {
23129
23130
  // @ts-expect-error - TS2322 - Type '{ dispay: string; flexDirection: "column"; color: string; }' is not assignable to type 'Properties<string | number, string & {}>'.
23130
23131
  dispay: "flex",
23131
23132
  flexDirection: "column",
23132
- color: Color.offBlack
23133
+ color: color.offBlack
23133
23134
  },
23134
23135
  className: descriptionClassName
23135
23136
  }, /*#__PURE__*/React.createElement("div", {
@@ -23172,7 +23173,7 @@ const Choice = function Choice(props) {
23172
23173
  disabled: disabled || apiOptions.readOnly,
23173
23174
  style: {
23174
23175
  flex: 1,
23175
- color: Color.offBlack,
23176
+ color: color.offBlack,
23176
23177
  userSelect: "text"
23177
23178
  },
23178
23179
  ref: forwardedRef,
@@ -23187,9 +23188,9 @@ const Choice = function Choice(props) {
23187
23188
  flexDirection: "row",
23188
23189
  justifyContent: "center",
23189
23190
  alignContent: "center",
23190
- paddingTop: Spacing.xSmall_8,
23191
- paddingBottom: Spacing.xSmall_8,
23192
- paddingLeft: Spacing.xSmall_8
23191
+ paddingTop: spacing.xSmall_8,
23192
+ paddingBottom: spacing.xSmall_8,
23193
+ paddingLeft: spacing.xSmall_8
23193
23194
  }
23194
23195
  }, /*#__PURE__*/React.createElement(ChoiceIcon, {
23195
23196
  pos: pos,
@@ -23205,7 +23206,7 @@ const Choice = function Choice(props) {
23205
23206
  previouslyAnswered: previouslyAnswered
23206
23207
  }), /*#__PURE__*/React.createElement("span", {
23207
23208
  style: {
23208
- paddingLeft: Spacing.small_12,
23209
+ paddingLeft: spacing.small_12,
23209
23210
  textAlign: "left",
23210
23211
  flex: 1,
23211
23212
  paddingTop: 4
@@ -23225,7 +23226,7 @@ const Choice = function Choice(props) {
23225
23226
  content: i18n._("Cross out option"),
23226
23227
  closeButtonVisible: true,
23227
23228
  actions: /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Strut, {
23228
- size: Spacing.medium_16
23229
+ size: spacing.medium_16
23229
23230
  }), /*#__PURE__*/React.createElement(Button, {
23230
23231
  kind: "primary",
23231
23232
  "aria-label": i18n._("Cross out Choice %(letter)s", {
@@ -23273,7 +23274,7 @@ const Choice = function Choice(props) {
23273
23274
  }) => /*#__PURE__*/React.createElement(Icon, {
23274
23275
  icon: ellipsisHorizontalIcon,
23275
23276
  size: 3,
23276
- color: Color.offBlack64
23277
+ color: color.offBlack64
23277
23278
  })))), showRationale && /*#__PURE__*/React.createElement("div", {
23278
23279
  className: rationaleClassName,
23279
23280
  "data-test-id": `perseus-radio-rationale-content-${pos}`
@@ -23289,7 +23290,7 @@ const styles$u = StyleSheet.create({
23289
23290
  padding: intermediateCheckboxPadding,
23290
23291
  paddingTop: 0,
23291
23292
  marginLeft: 54,
23292
- color: Color.offBlack64,
23293
+ color: color.offBlack64,
23293
23294
  [mediaQueries.smOrSmaller]: {
23294
23295
  padding: intermediateCheckboxPaddingPhone,
23295
23296
  paddingTop: 0
@@ -23300,14 +23301,14 @@ var Choice$1 = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.
23300
23301
  forwardedRef: ref
23301
23302
  })));
23302
23303
 
23303
- const _excluded$2 = ["showContent", "content", "forwardedRef"];
23304
+ const _excluded$3 = ["showContent", "content", "forwardedRef"];
23304
23305
  const ChoiceNoneAbove = function ChoiceNoneAbove(props) {
23305
23306
  const {
23306
23307
  showContent,
23307
23308
  content,
23308
23309
  forwardedRef
23309
23310
  } = props,
23310
- rest = _objectWithoutPropertiesLoose(props, _excluded$2);
23311
+ rest = _objectWithoutPropertiesLoose(props, _excluded$3);
23311
23312
  const choiceProps = _extends({}, rest, {
23312
23313
  content: showContent ? content :
23313
23314
  /*#__PURE__*/
@@ -24687,8 +24688,8 @@ class Definition extends React.Component {
24687
24688
  pressed
24688
24689
  }) => /*#__PURE__*/React.createElement("span", {
24689
24690
  style: {
24690
- color: Color.blue,
24691
- borderBottom: hovered || focused || pressed ? `2px solid ${Color.blue}` : "none"
24691
+ color: color.blue,
24692
+ borderBottom: hovered || focused || pressed ? `2px solid ${color.blue}` : "none"
24692
24693
  }
24693
24694
  }, this.props.togglePrompt))));
24694
24695
  }
@@ -24699,7 +24700,7 @@ Definition.defaultProps = {
24699
24700
  };
24700
24701
  const styles$q = {
24701
24702
  tooltipBody: {
24702
- color: Color.offBlack,
24703
+ color: color.offBlack,
24703
24704
  fontSize: 20,
24704
24705
  fontWeight: 500,
24705
24706
  lineHeight: "30px"
@@ -24999,7 +25000,7 @@ class GradedGroupAnswerBar extends React.Component {
24999
25000
  onNextQuestion
25000
25001
  } = this.props;
25001
25002
  const answerBarStyle = _extends({}, styles$o.answerBar, {
25002
- backgroundColor: answerBarState === "CORRECT" ? Color.offWhite : "white",
25003
+ backgroundColor: answerBarState === "CORRECT" ? color.offWhite : "white",
25003
25004
  // Center the "Correct!" message only when there's no next question
25004
25005
  justifyContent: answerBarState === "CORRECT" && !onNextQuestion ? "center" : "space-between"
25005
25006
  });
@@ -25029,7 +25030,7 @@ class GradedGroupAnswerBar extends React.Component {
25029
25030
  }, /*#__PURE__*/React.createElement("span", {
25030
25031
  style: {
25031
25032
  fontSize: 28,
25032
- color: Color.green
25033
+ color: color.green
25033
25034
  }
25034
25035
  }, /*#__PURE__*/React.createElement(InlineIcon, _extends({}, iconStar, {
25035
25036
  style: {
@@ -25059,7 +25060,7 @@ const styles$o = {
25059
25060
  marginTop: phoneMargin,
25060
25061
  paddingLeft: phoneMargin,
25061
25062
  paddingRight: 10,
25062
- borderTop: `1px solid ${Color.offBlack50}`
25063
+ borderTop: `1px solid ${color.offBlack50}`
25063
25064
  },
25064
25065
  tryAgainIcon: {
25065
25066
  fontSize: 28,
@@ -25361,7 +25362,7 @@ const styles$n = StyleSheet.create({
25361
25362
  padding: 0,
25362
25363
  border: "none",
25363
25364
  marginTop: 20,
25364
- color: Color.blue,
25365
+ color: color.blue,
25365
25366
  cursor: "pointer",
25366
25367
  display: "block",
25367
25368
  clear: "both"
@@ -25369,7 +25370,7 @@ const styles$n = StyleSheet.create({
25369
25370
  explanationTitle: {
25370
25371
  backgroundColor: "unset",
25371
25372
  marginTop: 20,
25372
- color: Color.blue,
25373
+ color: color.blue,
25373
25374
  marginBottom: 10,
25374
25375
  cursor: "pointer",
25375
25376
  fontSize: 14,
@@ -25578,7 +25579,7 @@ const styles$m = StyleSheet.create({
25578
25579
  },
25579
25580
  title: {
25580
25581
  fontSize: 12,
25581
- color: Color.offBlack64,
25582
+ color: color.offBlack64,
25582
25583
  textTransform: "uppercase",
25583
25584
  marginBottom: 11,
25584
25585
  letterSpacing: 0.8
@@ -25612,7 +25613,7 @@ const styles$m = StyleSheet.create({
25612
25613
  height: 10,
25613
25614
  borderRadius: "100%",
25614
25615
  borderWidth: 2,
25615
- borderColor: Color.blue,
25616
+ borderColor: color.blue,
25616
25617
  borderStyle: "solid"
25617
25618
  },
25618
25619
  indicatorDotFocused: {
@@ -25620,7 +25621,7 @@ const styles$m = StyleSheet.create({
25620
25621
  borderStyle: "double"
25621
25622
  },
25622
25623
  indicatorDotActive: {
25623
- backgroundColor: Color.blue,
25624
+ backgroundColor: color.blue,
25624
25625
  width: "100%",
25625
25626
  height: "100%"
25626
25627
  },
@@ -28216,6 +28217,58 @@ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.
28216
28217
  return (coord + ranges[i][1]) / extent;
28217
28218
  }));
28218
28219
 
28220
+ const useTransform = (...points) => {
28221
+ const {
28222
+ viewTransform,
28223
+ userTransform
28224
+ } = useTransformContext();
28225
+ const transformToPx = vec.matrixMult(viewTransform, userTransform);
28226
+ const transformedPoints = points.map(point => vec.transform(point, transformToPx));
28227
+ return transformedPoints;
28228
+ };
28229
+
28230
+ /**
28231
+ * Given two points, find the tips that extends through the points to the edge of the range.
28232
+ * @param collinearPoint - The point that the line passes through. Needed to establish slope.
28233
+ * @param extendFrom - The point that the line extends from to the edge of the graph.
28234
+ */
28235
+ const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
28236
+ // edges of the graph
28237
+ const [[xMin, xMax], [yMin, yMax]] = range;
28238
+ const [aX, aY] = collinearPoint;
28239
+ const [bX, bY] = extendFrom;
28240
+ const yDiff = bY - aY;
28241
+ const xDiff = bX - aX;
28242
+ const slope = yDiff / xDiff;
28243
+ const yAtXMin = slope * (xMin - aX) + aY;
28244
+ const yAtXMax = slope * (xMax - aX) + aY;
28245
+ const xAtYMin = (yMin - aY) / slope + aX;
28246
+ const xAtYMax = (yMax - aY) / slope + aX;
28247
+
28248
+ // clock analogy to describe quadrants
28249
+ switch (true) {
28250
+ // 12 o'clock to 2:59
28251
+ case yDiff > 0 && xDiff >= 0:
28252
+ return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
28253
+ // 3 o'clock to 5:59
28254
+ case yDiff <= 0 && xDiff > 0:
28255
+ // xAtYMin evaluates to -Infinity here, so we use absolute value
28256
+ return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
28257
+ // 9 o'clock to 11:59
28258
+ case yDiff >= 0 && xDiff < 0:
28259
+ return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
28260
+ // 6 o'clock to 8:59
28261
+ case yDiff < 0 && xDiff <= 0:
28262
+ return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
28263
+ default:
28264
+ return [xMax, yAtXMax];
28265
+ }
28266
+ };
28267
+ const getLines = points => points.map((point, i) => {
28268
+ const next = points[(i + 1) % points.length];
28269
+ return [point, next];
28270
+ });
28271
+
28219
28272
  const defaultStroke = "var(--movable-line-stroke-color)";
28220
28273
  const MovableLine = props => {
28221
28274
  const {
@@ -28226,18 +28279,12 @@ const MovableLine = props => {
28226
28279
  stroke = defaultStroke
28227
28280
  } = props;
28228
28281
  const midpoint = vec.midpoint(start, end);
28229
- const {
28230
- viewTransform,
28231
- userTransform
28232
- } = useTransformContext();
28233
- const transformToPx = vec.matrixMult(viewTransform, userTransform);
28234
- const startPtPx = vec.transform(start, transformToPx);
28235
- const endPtPx = vec.transform(end, transformToPx);
28282
+ const [startPtPx, endPtPx] = useTransform(start, end);
28236
28283
  let startExtend = undefined;
28237
28284
  let endExtend = undefined;
28238
28285
  if (extend) {
28239
- startExtend = extend.start ? getExtensionCoords(end, start, extend.range) : undefined;
28240
- endExtend = extend.end ? getExtensionCoords(start, end, extend.range) : undefined;
28286
+ startExtend = extend.start ? getRayIntersectionCoords(end, start, extend.range) : undefined;
28287
+ endExtend = extend.end ? getRayIntersectionCoords(start, end, extend.range) : undefined;
28241
28288
  }
28242
28289
  const line = useRef(null);
28243
28290
  const {
@@ -28299,50 +28346,12 @@ function SVGLine(props) {
28299
28346
  });
28300
28347
  }
28301
28348
 
28302
- /**
28303
- * Given two points, find the tips that extends through the points to the edge of the range.
28304
- * @param collinearPoint - The point that the line passes through. Needed to establish slope.
28305
- * @param extendFrom - The point that the line extends from to the edge of the graph.
28306
- */
28307
- const getExtensionCoords = (collinearPoint, extendFrom, range) => {
28308
- // edges of the graph
28309
- const [[xMin, xMax], [yMin, yMax]] = range;
28310
- const [aX, aY] = collinearPoint;
28311
- const [bX, bY] = extendFrom;
28312
- const yDiff = bY - aY;
28313
- const xDiff = bX - aX;
28314
- const slope = yDiff / xDiff;
28315
- const yAtXMin = slope * (xMin - aX) + aY;
28316
- const yAtXMax = slope * (xMax - aX) + aY;
28317
- const xAtYMin = (yMin - aY) / slope + aX;
28318
- const xAtYMax = (yMax - aY) / slope + aX;
28319
-
28320
- // clock analogy to describe quadrants
28321
- switch (true) {
28322
- // 12 o'clock to 2:59
28323
- case yDiff > 0 && xDiff >= 0:
28324
- return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
28325
- // 3 o'clock to 5:59
28326
- case yDiff <= 0 && xDiff > 0:
28327
- // xAtYMin evaluates to -Infinity here, so we use absolute value
28328
- return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
28329
- // 9 o'clock to 11:59
28330
- case yDiff >= 0 && xDiff < 0:
28331
- return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
28332
- // 6 o'clock to 8:59
28333
- case yDiff < 0 && xDiff <= 0:
28334
- return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
28335
- default:
28336
- return [xMax, yAtXMax];
28337
- }
28338
- };
28339
-
28340
28349
  const StyledMovablePoint = props => {
28341
28350
  const hitboxRef = useRef(null);
28342
28351
  const {
28343
28352
  point,
28344
28353
  onMove,
28345
- color = Color.blue
28354
+ color: color$1 = color.blue
28346
28355
  } = props;
28347
28356
  const {
28348
28357
  dragging
@@ -28352,12 +28361,7 @@ const StyledMovablePoint = props => {
28352
28361
  onMove,
28353
28362
  constrain: p => p
28354
28363
  });
28355
- const {
28356
- viewTransform,
28357
- userTransform
28358
- } = useTransformContext();
28359
- const transformToPx = vec.matrixMult(viewTransform, userTransform);
28360
- const [x, y] = vec.transform(point, transformToPx);
28364
+ const [[x, y]] = useTransform(point);
28361
28365
  return /*#__PURE__*/React.createElement("g", {
28362
28366
  ref: hitboxRef,
28363
28367
  className: "movable-point",
@@ -28366,7 +28370,7 @@ const StyledMovablePoint = props => {
28366
28370
  cursor: dragging ? "grabbing" : "grab",
28367
28371
  touchAction: "none",
28368
28372
  outline: "none",
28369
- "--movable-point-color": color
28373
+ "--movable-point-color": color$1
28370
28374
  }
28371
28375
  }, /*#__PURE__*/React.createElement("circle", {
28372
28376
  className: "movable-point-hitbox",
@@ -28386,7 +28390,7 @@ const StyledMovablePoint = props => {
28386
28390
  cx: x,
28387
28391
  cy: y,
28388
28392
  style: {
28389
- fill: color
28393
+ fill: color$1
28390
28394
  }
28391
28395
  }));
28392
28396
  };
@@ -28524,6 +28528,167 @@ const RayGraph = props => {
28524
28528
  }));
28525
28529
  };
28526
28530
 
28531
+ /**
28532
+ * Wrapper with Mafs transform styles for SVG elements.
28533
+ * Scales and translates the SVG element to match the Mafs view.
28534
+ * Use if you do not need/want to use the hook in `use-transform.ts`.
28535
+ */
28536
+ const MafsCssTransformWrapper = ({
28537
+ children
28538
+ }) => /*#__PURE__*/React.createElement("g", {
28539
+ style: {
28540
+ transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
28541
+ }
28542
+ }, children);
28543
+
28544
+ const _excluded$2 = ["children"];
28545
+ const TextLabel = _ref => {
28546
+ let {
28547
+ children
28548
+ } = _ref,
28549
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded$2);
28550
+ return /*#__PURE__*/React__default.createElement(Text$1, _extends({
28551
+ size: 16,
28552
+ svgTextProps: {
28553
+ filter: "url(#background)",
28554
+ fontWeight: "bold"
28555
+ }
28556
+ }, rest), children);
28557
+ };
28558
+ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*#__PURE__*/React__default.createElement("filter", {
28559
+ id: "background",
28560
+ x: "-5%",
28561
+ width: "110%",
28562
+ y: "0%",
28563
+ height: "100%"
28564
+ }, /*#__PURE__*/React__default.createElement("feFlood", {
28565
+ floodColor: "#FFF",
28566
+ floodOpacity: "0.64"
28567
+ }), /*#__PURE__*/React__default.createElement("feComposite", {
28568
+ operator: "over",
28569
+ in: "SourceGraphic"
28570
+ })));
28571
+
28572
+ const Angle = ({
28573
+ centerPoint,
28574
+ endPoints,
28575
+ range,
28576
+ polygonLines,
28577
+ showAngles
28578
+ }) => {
28579
+ const [centerX, centerY] = centerPoint;
28580
+ const areClockwise = clockwise$1([centerPoint, ...endPoints]);
28581
+ const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
28582
+ const radius = 0.3;
28583
+ const a = vec.dist(centerPoint, endPoints[0]);
28584
+ const b = vec.dist(centerPoint, endPoints[1]);
28585
+ const c = vec.dist(endPoints[0], endPoints[1]);
28586
+
28587
+ // Law of cosines
28588
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
28589
+ const y1 = centerY + (startY - centerY) / a * radius;
28590
+ const x2 = centerX + (endX - centerX) / b * radius;
28591
+ const x1 = centerX + (startX - centerX) / a * radius;
28592
+ const y2 = centerY + (endY - centerY) / b * radius;
28593
+
28594
+ // Fourth point that would create a rhomboid
28595
+ // Used to calculate the inside of the angle and to create square
28596
+ const [x3, y3] = vec.add(centerPoint, vec.add(vec.sub([x1, y1], centerPoint), vec.sub([x2, y2], centerPoint)));
28597
+ if (!showAngles) {
28598
+ return isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
28599
+ start: [x1, y1],
28600
+ vertex: [x2, y2],
28601
+ end: [x3, y3]
28602
+ }) : null;
28603
+ }
28604
+
28605
+ // Midpoint betwen ends of arc
28606
+ const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
28607
+ const largeArcFlag = isOutside ? 1 : 0;
28608
+ const sweepFlag = isOutside ? 1 : 0;
28609
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
28610
+ let angleInDegrees = angle * (180 / Math.PI);
28611
+ // If we have triggered "largArcFlag", the angle should be greater than 180
28612
+ if (isOutside) {
28613
+ angleInDegrees = 360 - angleInDegrees;
28614
+ }
28615
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
28616
+ const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
28617
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
28618
+ id: "background",
28619
+ x: "-5%",
28620
+ width: "110%",
28621
+ y: "0%",
28622
+ height: "100%"
28623
+ }, /*#__PURE__*/React.createElement("feFlood", {
28624
+ floodColor: "#FFF",
28625
+ floodOpacity: "0.5"
28626
+ }), /*#__PURE__*/React.createElement("feComposite", {
28627
+ operator: "over",
28628
+ in: "SourceGraphic"
28629
+ }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
28630
+ start: [x1, y1],
28631
+ vertex: [x2, y2],
28632
+ end: [x3, y3]
28633
+ }) : /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
28634
+ d: arc,
28635
+ strokeWidth: 0.02,
28636
+ fill: "none"
28637
+ })), /*#__PURE__*/React.createElement(TextLabel, {
28638
+ x: x3,
28639
+ y: y3,
28640
+ attach: y3 - centerY > 0 ? "s" : "n",
28641
+ attachDistance: Math.abs(y3 - centerY) < 0.2 || vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
28642
+ }, angleLabel, "\xB0"));
28643
+ };
28644
+
28645
+ /**
28646
+ * This is broken out into its own component so that it can be used for an early return
28647
+ * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
28648
+ */
28649
+ const RightAngleSquare = ({
28650
+ start: [x1, y1],
28651
+ vertex: [x2, y2],
28652
+ end: [x3, y3]
28653
+ }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
28654
+ d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
28655
+ strokeWidth: 0.02,
28656
+ fill: "none"
28657
+ }));
28658
+ const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
28659
+
28660
+ /**
28661
+ * Determines if an angle is an inside (false) or outside (true) angle.
28662
+ * They way, we know to flip the `largeArc` and `sweepArc` flags.
28663
+ * Uses the priciple that a ray from a point inside a polygon will intersect
28664
+ * with an odd number of lines, while a ray from a point outside the polygon
28665
+ * will intersect with an even number of lines.
28666
+ * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
28667
+ */
28668
+ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
28669
+ // Create a ray from the midpoint (inside angle) to the edge of the range
28670
+ const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
28671
+ let lineIntersectionCount = 0;
28672
+ polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
28673
+
28674
+ // If the number of intersections is even, the angle is inside the polygon
28675
+ return !isEven(lineIntersectionCount);
28676
+ };
28677
+
28678
+ // https://stackoverflow.com/a/24392281/7347484
28679
+ // The "intersects" function in geometry doesn't seem to work for this use case
28680
+ const linesIntersect = ([[a, b], [c, d]], [[p, q], [r, s]]) => {
28681
+ const determinant = (c - a) * (s - q) - (r - p) * (d - b);
28682
+ if (determinant === 0) {
28683
+ return false;
28684
+ } else {
28685
+ const lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / determinant;
28686
+ const gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / determinant;
28687
+ return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1;
28688
+ }
28689
+ };
28690
+ const isEven = n => n % 2 === 0;
28691
+
28527
28692
  const PolygonGraph = props => {
28528
28693
  var _points$length;
28529
28694
  const [focused, setFocused] = React.useState(false);
@@ -28533,7 +28698,10 @@ const PolygonGraph = props => {
28533
28698
  } = props;
28534
28699
  const {
28535
28700
  coords,
28536
- type
28701
+ type,
28702
+ showAngles,
28703
+ showSides,
28704
+ range
28537
28705
  } = props.graphState;
28538
28706
  const points = coords != null ? coords : [[0, 0]];
28539
28707
  const pointsSum = points.reduce((acc, point) => vec.add(acc, point), [0, 0]);
@@ -28551,12 +28719,36 @@ const PolygonGraph = props => {
28551
28719
  constrain: p => p
28552
28720
  });
28553
28721
  const active = hovered || focused || dragging;
28722
+ const lines = getLines(points);
28554
28723
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polygon, {
28555
28724
  points: [...points],
28556
28725
  color: "var(--movable-line-stroke-color)",
28557
28726
  svgPolygonProps: {
28558
28727
  strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)"
28559
28728
  }
28729
+ }), points.map((point, i) => {
28730
+ const pt1 = points.at(i - 1);
28731
+ const pt2 = points[(i + 1) % points.length];
28732
+ if (!pt1 || !pt2) {
28733
+ return null;
28734
+ }
28735
+ return /*#__PURE__*/React.createElement(Angle, {
28736
+ key: "angle-" + i,
28737
+ centerPoint: point,
28738
+ endPoints: [pt1, pt2],
28739
+ active: active,
28740
+ range: range,
28741
+ polygonLines: lines,
28742
+ showAngles: !!showAngles
28743
+ });
28744
+ }), showSides && lines.map(([start, end], i) => {
28745
+ const [x, y] = vec.midpoint(start, end);
28746
+ const length = parseFloat(vec.dist(start, end).toFixed(1));
28747
+ return /*#__PURE__*/React.createElement(TextLabel, {
28748
+ key: "side-" + i,
28749
+ x: x,
28750
+ y: y
28751
+ }, !Number.isInteger(length) && "≈ ", length);
28560
28752
  }), /*#__PURE__*/React.createElement(Polygon, {
28561
28753
  points: [...points],
28562
28754
  color: "transparent",
@@ -28574,7 +28766,7 @@ const PolygonGraph = props => {
28574
28766
  className: "movable-polygon"
28575
28767
  }
28576
28768
  }), points.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
28577
- key: i,
28769
+ key: "point-" + i,
28578
28770
  point: point,
28579
28771
  onMove: destination => dispatch(moveControlPoint(i, destination)),
28580
28772
  "data-testid": type + i
@@ -29256,7 +29448,10 @@ var components = /*#__PURE__*/Object.freeze({
29256
29448
  * If a graphie URL is provided in `backgroundImage`, will return the rendered graphie background.
29257
29449
  * Otherwise, returns `null`.
29258
29450
  */
29259
- const getLegacyGrid = (box, backgroundImage) => {
29451
+ const LegacyGrid = ({
29452
+ box,
29453
+ backgroundImage
29454
+ }) => {
29260
29455
  const {
29261
29456
  url,
29262
29457
  width,
@@ -29533,17 +29728,17 @@ function initializeGraphState(params) {
29533
29728
  })
29534
29729
  };
29535
29730
  case "polygon":
29536
- return {
29537
- type: "polygon",
29731
+ return _extends({
29538
29732
  hasBeenInteractedWith: false,
29539
29733
  range,
29540
- snapStep,
29734
+ snapStep
29735
+ }, graph, {
29541
29736
  coords: getPolygonCoords({
29542
29737
  graph,
29543
29738
  range,
29544
29739
  step
29545
29740
  })
29546
- };
29741
+ });
29547
29742
  case "angle":
29548
29743
  case "point":
29549
29744
  case "circle":
@@ -29558,30 +29753,39 @@ function initializeGraphState(params) {
29558
29753
  // TS v4 doesn't narrow return types, while v5 does.
29559
29754
  // Instead of updating to v5, using generic type to relate input and output types.
29560
29755
  function getGradableGraph(state, initialGraph) {
29561
- var _state$coords;
29562
29756
  if (!state.hasBeenInteractedWith) {
29563
- return initialGraph;
29757
+ return {
29758
+ type: initialGraph.type
29759
+ };
29564
29760
  }
29565
- switch (true) {
29566
- // coords: Array of CollinearTuple
29567
- case state.type === "linear-system" && initialGraph.type === "linear-system":
29568
- case state.type === "segment" && initialGraph.type === "segment":
29569
- return _extends({}, initialGraph, {
29570
- coords: state.coords
29571
- });
29572
- // coords: CollinearTuple
29573
- case state.type === "linear" && initialGraph.type === "linear":
29574
- case state.type === "ray" && initialGraph.type === "ray":
29575
- return _extends({}, initialGraph, {
29576
- coords: (_state$coords = state.coords) == null ? void 0 : _state$coords[0]
29577
- });
29578
- case state.type === "polygon" && initialGraph.type === "polygon":
29579
- return _extends({}, initialGraph, {
29580
- coords: state.coords
29581
- });
29582
- default:
29583
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29761
+ if (initialGraph.type === "linear-system" && state.type === "linear-system") {
29762
+ return _extends({}, initialGraph, {
29763
+ coords: state.coords
29764
+ });
29765
+ }
29766
+ if (state.type === "segment" && initialGraph.type === "segment") {
29767
+ return _extends({}, initialGraph, {
29768
+ coords: state.coords
29769
+ });
29770
+ }
29771
+ if (state.type === "linear" && initialGraph.type === "linear") {
29772
+ var _state$coords;
29773
+ return _extends({}, initialGraph, {
29774
+ coords: (_state$coords = state.coords) == null ? void 0 : _state$coords[0]
29775
+ });
29776
+ }
29777
+ if (state.type === "ray" && initialGraph.type === "ray") {
29778
+ var _state$coords2;
29779
+ return _extends({}, initialGraph, {
29780
+ coords: (_state$coords2 = state.coords) == null ? void 0 : _state$coords2[0]
29781
+ });
29782
+ }
29783
+ if (state.type === "polygon" && initialGraph.type === "polygon") {
29784
+ return _extends({}, initialGraph, {
29785
+ coords: state.coords
29786
+ });
29584
29787
  }
29788
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29585
29789
  }
29586
29790
  const getDefaultSegments = ({
29587
29791
  graph,
@@ -29604,9 +29808,10 @@ const getDefaultSegments = ({
29604
29808
  return [5];
29605
29809
  }
29606
29810
  };
29811
+ const defaultRange = [[-10, 10], [-10, 10]];
29607
29812
  return ys(graph.numSegments).map(y => {
29608
29813
  let endpoints = [[-5, y], [5, y]];
29609
- endpoints = normalizeCoords(endpoints, range);
29814
+ endpoints = normalizeCoords(endpoints, defaultRange);
29610
29815
  endpoints = normalizePoints(range, step, endpoints);
29611
29816
  return endpoints;
29612
29817
  });
@@ -29688,7 +29893,6 @@ const renderGraph = props => {
29688
29893
  };
29689
29894
  const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29690
29895
  const [width, height] = props.box;
29691
- const legacyGrid = getLegacyGrid([width, height], props.backgroundImage);
29692
29896
  const [state, dispatch] = React.useReducer(interactiveGraphReducer, props, initializeGraphState);
29693
29897
  React.useImperativeHandle(ref, () => ({
29694
29898
  getUserInput: () => getGradableGraph(state, props.graph)
@@ -29699,13 +29903,17 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29699
29903
  height,
29700
29904
  position: "relative"
29701
29905
  }
29702
- }, legacyGrid, /*#__PURE__*/React.createElement(View, {
29906
+ }, /*#__PURE__*/React.createElement(LegacyGrid, {
29907
+ box: props.box,
29908
+ backgroundImage: props.backgroundImage
29909
+ }), /*#__PURE__*/React.createElement(View, {
29703
29910
  style: {
29704
29911
  position: "absolute",
29705
29912
  bottom: 0,
29706
29913
  left: 0
29707
29914
  }
29708
29915
  }, /*#__PURE__*/React.createElement(Mafs, {
29916
+ preserveAspectRatio: false,
29709
29917
  viewBox: {
29710
29918
  x: props.range[0],
29711
29919
  y: props.range[1],
@@ -29715,7 +29923,7 @@ const MafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
29715
29923
  zoom: false,
29716
29924
  width: width,
29717
29925
  height: height
29718
- }, !legacyGrid && /*#__PURE__*/React.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
29926
+ }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
29719
29927
  lockedFigures: props.lockedFigures
29720
29928
  }), renderGraph({
29721
29929
  state,
@@ -31369,18 +31577,18 @@ class InteractiveGraph extends React.Component {
31369
31577
  const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
31370
31578
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
31371
31579
  }
31372
- static validate(
31373
- // TODO(kevinb): rename state to userInput
31374
- // state === userInput
31375
- state, rubric, component) {
31580
+ static validate(userInput, rubric, component) {
31376
31581
  // When nothing has moved, there will neither be coords nor the
31377
31582
  // circle's center/radius fields. When those fields are absent, skip
31378
31583
  // all these checks; just go mark the answer as empty.
31379
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
31380
- const hasValue = !!(state.coords || state.center && state.radius);
31381
- if (state.type === rubric.correct.type && hasValue) {
31382
- if (state.type === "linear" && rubric.correct.type === "linear" && state.coords != null) {
31383
- const guess = state.coords;
31584
+ const hasValue = Boolean(
31585
+ // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
31586
+ userInput.coords ||
31587
+ // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
31588
+ userInput.center && userInput.radius);
31589
+ if (userInput.type === rubric.correct.type && hasValue) {
31590
+ if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
31591
+ const guess = userInput.coords;
31384
31592
  const correct = rubric.correct.coords;
31385
31593
 
31386
31594
  // If both of the guess points are on the correct line, it's
@@ -31397,8 +31605,8 @@ class InteractiveGraph extends React.Component {
31397
31605
  message: null
31398
31606
  };
31399
31607
  }
31400
- } else if (state.type === "linear-system" && rubric.correct.type === "linear-system" && state.coords != null) {
31401
- const guess = state.coords;
31608
+ } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
31609
+ const guess = userInput.coords;
31402
31610
  const correct = rubric.correct.coords;
31403
31611
  if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
31404
31612
  return {
@@ -31408,9 +31616,9 @@ class InteractiveGraph extends React.Component {
31408
31616
  message: null
31409
31617
  };
31410
31618
  }
31411
- } else if (state.type === "quadratic" && rubric.correct.type === "quadratic" && state.coords != null) {
31619
+ } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
31412
31620
  // If the parabola coefficients match, it's correct.
31413
- const guessCoeffs = this.getQuadraticCoefficients(state.coords);
31621
+ const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
31414
31622
  const correctCoeffs = this.getQuadraticCoefficients(
31415
31623
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31416
31624
  rubric.correct.coords);
@@ -31422,8 +31630,8 @@ class InteractiveGraph extends React.Component {
31422
31630
  message: null
31423
31631
  };
31424
31632
  }
31425
- } else if (state.type === "sinusoid" && rubric.correct.type === "sinusoid" && state.coords != null) {
31426
- const guessCoeffs = this.getSinusoidCoefficients(state.coords);
31633
+ } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
31634
+ const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
31427
31635
  const correctCoeffs = this.getSinusoidCoefficients(
31428
31636
  // @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
31429
31637
  rubric.correct.coords);
@@ -31438,8 +31646,8 @@ class InteractiveGraph extends React.Component {
31438
31646
  message: null
31439
31647
  };
31440
31648
  }
31441
- } else if (state.type === "circle" && rubric.correct.type === "circle") {
31442
- if (deepEq$1(state.center, rubric.correct.center) && eq(state.radius, rubric.correct.radius)) {
31649
+ } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
31650
+ if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
31443
31651
  return {
31444
31652
  type: "points",
31445
31653
  earned: 1,
@@ -31447,10 +31655,10 @@ class InteractiveGraph extends React.Component {
31447
31655
  message: null
31448
31656
  };
31449
31657
  }
31450
- } else if (state.type === "point" && rubric.correct.type === "point" && state.coords != null) {
31451
- var _state$coords;
31658
+ } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
31659
+ var _userInput$coords;
31452
31660
  let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
31453
- const guess = (_state$coords = state.coords) == null ? void 0 : _state$coords.slice();
31661
+ const guess = (_userInput$coords = userInput.coords) == null ? void 0 : _userInput$coords.slice();
31454
31662
  correct = correct.slice();
31455
31663
  // Everything's already rounded so we shouldn't need to do an
31456
31664
  // eq() comparison but _.isEqual(0, -0) is false, so we'll use
@@ -31467,9 +31675,9 @@ class InteractiveGraph extends React.Component {
31467
31675
  message: null
31468
31676
  };
31469
31677
  }
31470
- } else if (state.type === "polygon" && rubric.correct.type === "polygon" && state.coords != null) {
31471
- var _state$coords2, _rubric$correct$coord;
31472
- const guess = (_state$coords2 = state.coords) == null ? void 0 : _state$coords2.slice();
31678
+ } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
31679
+ var _userInput$coords2, _rubric$correct$coord;
31680
+ const guess = (_userInput$coords2 = userInput.coords) == null ? void 0 : _userInput$coords2.slice();
31473
31681
  // @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
31474
31682
  const correct = (_rubric$correct$coord = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord.slice();
31475
31683
  let match;
@@ -31493,9 +31701,9 @@ class InteractiveGraph extends React.Component {
31493
31701
  message: null
31494
31702
  };
31495
31703
  }
31496
- } else if (state.type === "segment" && rubric.correct.type === "segment" && state.coords != null) {
31704
+ } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
31497
31705
  var _rubric$correct$coord2;
31498
- let guess = state.coords.slice();
31706
+ let guess = userInput.coords.slice();
31499
31707
  let correct = (_rubric$correct$coord2 = rubric.correct.coords) == null ? void 0 : _rubric$correct$coord2.slice();
31500
31708
  guess = _.invoke(guess, "sort").sort();
31501
31709
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
@@ -31508,8 +31716,8 @@ class InteractiveGraph extends React.Component {
31508
31716
  message: null
31509
31717
  };
31510
31718
  }
31511
- } else if (state.type === "ray" && rubric.correct.type === "ray" && state.coords != null) {
31512
- const guess = state.coords;
31719
+ } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
31720
+ const guess = userInput.coords;
31513
31721
  const correct = rubric.correct.coords;
31514
31722
  if (
31515
31723
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
@@ -31523,8 +31731,8 @@ class InteractiveGraph extends React.Component {
31523
31731
  message: null
31524
31732
  };
31525
31733
  }
31526
- } else if (state.type === "angle" && rubric.correct.type === "angle") {
31527
- const guess = state.coords;
31734
+ } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
31735
+ const guess = userInput.coords;
31528
31736
  const correct = rubric.correct.coords;
31529
31737
  let match;
31530
31738
  if (rubric.correct.match === "congruent") {
@@ -31563,7 +31771,7 @@ class InteractiveGraph extends React.Component {
31563
31771
 
31564
31772
  // The input wasn't correct, so check if it's a blank input or if it's
31565
31773
  // actually just wrong
31566
- if (!hasValue || _.isEqual(state, rubric.graph)) {
31774
+ if (!hasValue || _.isEqual(userInput, rubric.graph)) {
31567
31775
  // We're where we started.
31568
31776
  return {
31569
31777
  type: "invalid",
@@ -32650,7 +32858,7 @@ const styles$g = StyleSheet.create({
32650
32858
  });
32651
32859
 
32652
32860
  const BringToFront = {
32653
- boxShadow: `0 8px 8px ${Color.offBlack64}`,
32861
+ boxShadow: `0 8px 8px ${color.offBlack64}`,
32654
32862
  zIndex: 1000
32655
32863
  };
32656
32864
  const AnswerPill = props => {
@@ -32703,7 +32911,7 @@ const styles$f = StyleSheet.create({
32703
32911
  backgroundColor: "#00880b"
32704
32912
  },
32705
32913
  incorrect: {
32706
- backgroundColor: Color.offBlack64
32914
+ backgroundColor: color.offBlack64
32707
32915
  },
32708
32916
  pill: {
32709
32917
  // Reset the Pill's default height in order to account
@@ -32746,7 +32954,7 @@ class Marker extends React.Component {
32746
32954
  // default dot
32747
32955
  let args = {
32748
32956
  size: MARKER_SIZE,
32749
- color: Color.white,
32957
+ color: color.white,
32750
32958
  icon: iconNull
32751
32959
  };
32752
32960
  if (showCorrectness) {
@@ -32809,7 +33017,7 @@ Marker.defaultProps = {
32809
33017
  const styles$e = StyleSheet.create({
32810
33018
  marker: {
32811
33019
  position: "absolute",
32812
- backgroundColor: Color.white,
33020
+ backgroundColor: color.white,
32813
33021
  borderRadius: MARKER_SIZE,
32814
33022
  // Center marker position based on its maximum size.
32815
33023
  width: MARKER_SIZE,
@@ -32817,7 +33025,7 @@ const styles$e = StyleSheet.create({
32817
33025
  marginLeft: MARKER_SIZE / -2,
32818
33026
  marginTop: MARKER_SIZE / -2,
32819
33027
  // Add a shadow to the marker to make it stand out from the image.
32820
- boxShadow: `0 8px 8px ${Color.offBlack8}`
33028
+ boxShadow: `0 8px 8px ${color.offBlack8}`
32821
33029
  },
32822
33030
  // The base and unfilled marker style.
32823
33031
  markerIcon: {
@@ -32827,7 +33035,7 @@ const styles$e = StyleSheet.create({
32827
33035
  boxSizing: "border-box",
32828
33036
  width: MARKER_SIZE,
32829
33037
  height: MARKER_SIZE,
32830
- border: `2px solid ${Color.offBlack64}`,
33038
+ border: `2px solid ${color.offBlack64}`,
32831
33039
  borderRadius: MARKER_SIZE
32832
33040
  },
32833
33041
  // The animation that presents the marker to the learner
@@ -32835,11 +33043,11 @@ const styles$e = StyleSheet.create({
32835
33043
  animationName: {
32836
33044
  "0%": {
32837
33045
  transform: "scale(1)",
32838
- backgroundColor: Color.blue
33046
+ backgroundColor: color.blue
32839
33047
  },
32840
33048
  "100%": {
32841
33049
  transform: "scale(1.3)",
32842
- backgroundColor: Color.blue
33050
+ backgroundColor: color.blue
32843
33051
  }
32844
33052
  },
32845
33053
  animationDirection: "alternate",
@@ -32856,35 +33064,35 @@ const styles$e = StyleSheet.create({
32856
33064
  animationIterationCount: "2"
32857
33065
  },
32858
33066
  markerActive: {
32859
- outline: `2px solid ${Color.blue}`,
33067
+ outline: `2px solid ${color.blue}`,
32860
33068
  outlineOffset: 2
32861
33069
  },
32862
33070
  // The learner is making an initial selection
32863
33071
  markerSelected: {
32864
- boxShadow: `0 8px 8px ${Color.offBlack8}`,
32865
- border: `solid 4px ${Color.white}`,
32866
- backgroundColor: Color.blue,
33072
+ boxShadow: `0 8px 8px ${color.offBlack8}`,
33073
+ border: `solid 4px ${color.white}`,
33074
+ backgroundColor: color.blue,
32867
33075
  borderRadius: MARKER_SIZE,
32868
33076
  transform: "rotate(180deg)"
32869
33077
  },
32870
33078
  // The learner has made a selection
32871
33079
  markerFilled: {
32872
33080
  backgroundColor: "#ECF3FE",
32873
- border: `4px solid ${Color.blue}`
33081
+ border: `4px solid ${color.blue}`
32874
33082
  },
32875
33083
  markerGraded: {
32876
33084
  width: MARKER_SIZE,
32877
33085
  height: MARKER_SIZE,
32878
33086
  justifyContent: "center",
32879
33087
  alignItems: "center",
32880
- border: `2px solid ${Color.white}`
33088
+ border: `2px solid ${color.white}`
32881
33089
  },
32882
33090
  markerCorrect: {
32883
33091
  background: "#00880b" // WB green darkened by 18%
32884
33092
  },
32885
33093
 
32886
33094
  markerIncorrect: {
32887
- background: Color.offBlack64
33095
+ background: color.offBlack64
32888
33096
  }
32889
33097
  });
32890
33098
 
@@ -33313,6 +33521,7 @@ class LabelImage extends React.Component {
33313
33521
  const answerChoicesActive = index === activeMarkerIndex;
33314
33522
  const showAnswerChoice = marker.selected && !answerChoicesActive && !this.state.hideAnswers;
33315
33523
  return /*#__PURE__*/React.createElement(View, {
33524
+ key: index,
33316
33525
  style: {
33317
33526
  position: "absolute",
33318
33527
  left: `${marker.x}%`,
@@ -37911,7 +38120,7 @@ const styles$8 = StyleSheet.create({
37911
38120
  // and the "Add highlight" action fails.
37912
38121
  userSelect: "none",
37913
38122
  fontFamily: `"Lato", sans-serif`,
37914
- padding: `10px ${Spacing.medium_16}px`,
38123
+ padding: `10px ${spacing.medium_16}px`,
37915
38124
  cursor: "pointer"
37916
38125
  }
37917
38126
  });
@@ -41594,7 +41803,7 @@ const VideoTranscriptLink = props => {
41594
41803
  const video = (_result$data = result.data) == null ? void 0 : _result$data.video;
41595
41804
  return /*#__PURE__*/React.createElement(View, {
41596
41805
  style: styles$4.transcriptLink
41597
- }, /*#__PURE__*/React.createElement(Text$1, null, video == null ? void 0 : video.title), /*#__PURE__*/React.createElement(Strut, {
41806
+ }, /*#__PURE__*/React.createElement(Text$2, null, video == null ? void 0 : video.title), /*#__PURE__*/React.createElement(Strut, {
41598
41807
  size: 10
41599
41808
  }), /*#__PURE__*/React.createElement(Link, {
41600
41809
  href: "/transcript/" + ((video == null ? void 0 : video.contentId) || "videoNotFound"),
@@ -41763,7 +41972,7 @@ const init = function init(options) {
41763
41972
 
41764
41973
  // This file is processed by a Rollup plugin (replace) to inject the production
41765
41974
  const libName = "@khanacademy/perseus";
41766
- const libVersion = "20.8.0";
41975
+ const libVersion = "21.0.0";
41767
41976
  addLibraryVersionToPerseusDebug(libName, libVersion);
41768
41977
 
41769
41978
  const apiVersion = {
@@ -44015,6 +44224,140 @@ const registerAllWidgetsForTesting = () => {
44015
44224
  replaceDeprecatedWidgets();
44016
44225
  };
44017
44226
 
44227
+ // These are all the current widgets that require user-input and are supported for automatic scoring
44228
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44229
+
44230
+ /**
44231
+ * Regex for widget placeholders in a string.
44232
+ *
44233
+ * First capture group is the widget ID (ex. 'radio 1')
44234
+ * Second capture group is the widget type (ex. "radio)
44235
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44236
+ */
44237
+ function getWidgetRegex() {
44238
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
44239
+ }
44240
+
44241
+ /**
44242
+ * Add a widget placeholder using the widget ID.
44243
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
44244
+ *
44245
+ * @param {string} id
44246
+ * @returns {string}
44247
+ */
44248
+ function addWidget(id) {
44249
+ return `[[☃ ${id}]]`;
44250
+ }
44251
+
44252
+ /**
44253
+ * Get a widget type by a widget's ID
44254
+ * (please don't derive type from ID)
44255
+ *
44256
+ * @param {string} widgetId the ID of the widget
44257
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44258
+ * @returns {string} the widget type (ie "radio")
44259
+ */
44260
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44261
+ var _widget$type;
44262
+ const widget = widgetMap[WidgetId];
44263
+ return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
44264
+ }
44265
+
44266
+ /**
44267
+ * Does the content have a specific type of widget?
44268
+ *
44269
+ * @param {string} type the type of the widget in question (ie "radio")
44270
+ * @param {string} content the string to search through
44271
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44272
+ * @returns {boolean} if the content includes the widget type
44273
+ */
44274
+ function contentHasWidgetType(type, content, widgetMap) {
44275
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
44276
+ }
44277
+
44278
+ /**
44279
+ * Extract all widget IDs, which includes the widget type and instance number.
44280
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44281
+ *
44282
+ * Content should contain Perseus widget placeholders,
44283
+ * which look like: '[[☃ radio 1]]'.
44284
+ *
44285
+ * @param {string} content
44286
+ * @returns {ReadonlyArray<string>} widgetIds
44287
+ */
44288
+ function getWidgetIdsFromContent(content) {
44289
+ const widgets = [];
44290
+ const localWidgetRegex = getWidgetRegex();
44291
+ let match = localWidgetRegex.exec(content);
44292
+ while (match !== null) {
44293
+ widgets.push(match[1]);
44294
+ match = localWidgetRegex.exec(content);
44295
+ }
44296
+ return widgets;
44297
+ }
44298
+
44299
+ /**
44300
+ * Get a list of widget IDs from content,
44301
+ * but only for specific widget types
44302
+ *
44303
+ * @param {string} type the type of widget (ie "radio")
44304
+ * @param {string} content the string to parse
44305
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44306
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
44307
+ */
44308
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
44309
+ const rv = [];
44310
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
44311
+ widgetIdsInContent.forEach(widgetId => {
44312
+ const widget = widgetMap[widgetId];
44313
+ if ((widget == null ? void 0 : widget.type) === type) {
44314
+ rv.push(widgetId);
44315
+ }
44316
+ });
44317
+ return rv;
44318
+ }
44319
+
44320
+ /**
44321
+ * Pull the widget map out of ItemData
44322
+ *
44323
+ * @param {PerseusItem} PerseusItem containing a widgetMap
44324
+ * @returns {WidgetMap} the widget map in the PerseusItem
44325
+ */
44326
+ function getWidgetsMapFromItemData(itemData) {
44327
+ return itemData.question.widgets;
44328
+ }
44329
+
44330
+ /**
44331
+ * Get the widget information from a WidgetMap
44332
+ *
44333
+ * @param {string} widgetId the ID of the widget
44334
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
44335
+ */
44336
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
44337
+ var _widgetMap$widgetId;
44338
+ return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
44339
+ }
44340
+
44341
+ /**
44342
+ * Get select widgets from a widget map.
44343
+ * Useful for multi-items that needs to split
44344
+ * one PerseusItem into several
44345
+ *
44346
+ * @param {ReadonlyArray<string>} widgetIds to extract
44347
+ * @param {PerseusWidgetsMap} widgetMap to extract from
44348
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
44349
+ */
44350
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
44351
+ const widgets = {};
44352
+ widgetIds.forEach(widgetId => {
44353
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
44354
+ if (widget) {
44355
+ widgets[widgetId] = widget;
44356
+ }
44357
+ });
44358
+ return widgets;
44359
+ }
44360
+
44018
44361
  /**
44019
44362
  * This function extracts the answers from the widgets.
44020
44363
  *
@@ -44425,18 +44768,15 @@ const INDIVIDUAL_ANSWER_WIDGETS = ["interactive-graph", "categorizer", "grapher"
44425
44768
 
44426
44769
  /* Widgets that are supported for automatic scoring */
44427
44770
  const SUPPORTED_WIDGETS = ["radio", "numeric-input", "input-number", "expression", ...INDIVIDUAL_ANSWER_WIDGETS];
44428
- const getWidgetTypeFromWidgetKey = widgetKey => {
44429
- return widgetKey.split(" ")[0];
44430
- };
44431
44771
 
44432
44772
  /* Verify if the perseus item has supported widgets for automatic scoring */
44433
- const isWrongAnswerSupported = widgetKeys => {
44434
- return widgetKeys.length !== 0 && widgetKeys.every(widgetKey => SUPPORTED_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey)));
44773
+ const isWrongAnswerSupported = (widgetIds, widgetMap) => {
44774
+ return widgetIds.length !== 0 && widgetIds.every(widgetId => SUPPORTED_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap)));
44435
44775
  };
44436
44776
 
44437
- /* Verify if the widget key has an individual answer for the coach report view */
44438
- const shouldHaveIndividualAnswer = widgetKey => {
44439
- return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeFromWidgetKey(widgetKey));
44777
+ /* Verify if the widget ID has an individual answer for the coach report view */
44778
+ const shouldHaveIndividualAnswer = (widgetId, widgetMap) => {
44779
+ return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap));
44440
44780
  };
44441
44781
 
44442
44782
  /* Returns the answer userInput submission for currently supported widgets */
@@ -44455,29 +44795,30 @@ const getAnswerFromUserInput = (widgetType, userInput) => {
44455
44795
  return userInput;
44456
44796
  };
44457
44797
 
44458
- /* Returns the correct answer for a given widget key and Perseus Item */
44798
+ /* Returns the correct answer for a given widget ID and Perseus Item */
44459
44799
  // TODO (LEMS-1835): We should fix the resonse type from getWidget to be specific.
44460
44800
  // TODO (LEMS-1836): We should also consider adding the getOneCorrectAnswerFromRubric method to all widgets.
44461
- const getCorrectAnswerForWidgetKey = (widgetKey, itemData) => {
44462
- const rubric = itemData.question.widgets[widgetKey].options;
44463
- const widgetType = getWidgetTypeFromWidgetKey(widgetKey);
44801
+ const getCorrectAnswerForWidgetId = (widgetId, itemData) => {
44802
+ const rubric = itemData.question.widgets[widgetId].options;
44803
+ const widgetMap = getWidgetsMapFromItemData(itemData);
44804
+ const widgetType = getWidgetTypeByWidgetId(widgetId, widgetMap);
44464
44805
  const widget = getWidget(widgetType);
44465
44806
 
44466
44807
  // @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType<any>'.
44467
44808
  return widget == null || widget.getOneCorrectAnswerFromRubric == null ? void 0 : widget.getOneCorrectAnswerFromRubric(rubric);
44468
44809
  };
44469
44810
 
44470
- /* Verify if the widget key exists in the content string of the Perseus Item */
44471
- const isWidgetKeyInContent = (perseusItem, widgetKey) => {
44472
- return perseusItem.question.content.indexOf(widgetKey) !== -1;
44811
+ /* Verify if the widget ID exists in the content string of the Perseus Item */
44812
+ const isWidgetIdInContent = (perseusItem, widgetId) => {
44813
+ return perseusItem.question.content.indexOf(widgetId) !== -1;
44473
44814
  };
44474
44815
 
44475
- /* Return an array of all the widget keys that exist in the content string of a Perseus Item */
44476
- const getValidWidgetKeys = perseusItem => {
44816
+ /* Return an array of all the widget IDs that exist in the content string of a Perseus Item */
44817
+ const getValidWidgetIds = perseusItem => {
44477
44818
  const {
44478
44819
  widgets
44479
44820
  } = perseusItem.question;
44480
- return keys(widgets).filter(key => isWidgetKeyInContent(perseusItem, key));
44821
+ return keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
44481
44822
  };
44482
44823
 
44483
44824
  /**
@@ -44525,12 +44866,113 @@ const genericPerseusItemData = {
44525
44866
  * uses a basic Perseus object and updates it with custom values as needed.
44526
44867
  *
44527
44868
  * @param {Partial<PerseusItem>} customFields
44528
- * @returns PerseusItem
44869
+ * @returns {PerseusItem}
44529
44870
  */
44530
44871
  function generateTestPerseusItem(customFields = {}) {
44531
44872
  return _extends({}, genericPerseusItemData, customFields);
44532
44873
  }
44533
44874
 
44875
+ /**
44876
+ * Creates an object with the bare amount
44877
+ * of data to be a properly typed RadioWidget
44878
+ *
44879
+ * @returns {RadioWidget} skeleton RadioWidget for testing
44880
+ */
44881
+ function generateTestRadioWidget() {
44882
+ return {
44883
+ type: "radio",
44884
+ options: {
44885
+ choices: []
44886
+ }
44887
+ };
44888
+ }
44889
+
44890
+ /**
44891
+ * Creates an object with the bare amount
44892
+ * of data to be a properly typed InteractiveGraphWidget
44893
+ *
44894
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
44895
+ */
44896
+ function generateTestInteractiveGraphWidget() {
44897
+ return {
44898
+ type: "interactive-graph",
44899
+ options: {
44900
+ step: [1, 1],
44901
+ gridStep: [1, 1],
44902
+ snapStep: [1, 1],
44903
+ markings: "graph",
44904
+ labels: ["x", "y"],
44905
+ showProtractor: false,
44906
+ showRuler: false,
44907
+ rulerLabel: "mm",
44908
+ rulerTicks: 2,
44909
+ range: [[-10, 10], [-10, 10]],
44910
+ graph: {
44911
+ type: "angle"
44912
+ },
44913
+ correct: {
44914
+ type: "angle"
44915
+ }
44916
+ }
44917
+ };
44918
+ }
44919
+
44920
+ /**
44921
+ * Creates an object with the bare amount
44922
+ * of data to be a properly typed InteractiveGraphWidget
44923
+ *
44924
+ * @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
44925
+ */
44926
+ function generateTestCategorizerWidget() {
44927
+ return {
44928
+ type: "categorizer",
44929
+ options: {
44930
+ items: [],
44931
+ categories: [],
44932
+ randomizeItems: false,
44933
+ static: false,
44934
+ values: []
44935
+ }
44936
+ };
44937
+ }
44938
+
44939
+ /**
44940
+ * Creates an object with the bare amount
44941
+ * of data to be a properly typed ExpressionWidget
44942
+ *
44943
+ * @returns {ExpressionWidget} skeleton ExpressionWidget for testing
44944
+ */
44945
+ function generateTestExpressionWidget() {
44946
+ return {
44947
+ type: "expression",
44948
+ options: {
44949
+ answerForms: [],
44950
+ buttonSets: ["basic"],
44951
+ functions: [],
44952
+ times: false
44953
+ }
44954
+ };
44955
+ }
44956
+
44957
+ /**
44958
+ * Creates an object with the bare amount
44959
+ * of data to be a properly typed NumericInputWidget
44960
+ *
44961
+ * @returns {NumericInputWidget} skeleton NumericInputWidget for testing
44962
+ */
44963
+ function generateTestNumericInputWidget() {
44964
+ return {
44965
+ type: "numeric-input",
44966
+ options: {
44967
+ answers: [],
44968
+ labelText: "",
44969
+ size: "normal",
44970
+ coefficient: false,
44971
+ static: false
44972
+ }
44973
+ };
44974
+ }
44975
+
44534
44976
  /**
44535
44977
  * Some helpers for CEDAR logging. CEDAR convention dictates
44536
44978
  * that enum values should be upper case. Ex: for the
@@ -44585,163 +45027,6 @@ function convertWidgetNameToEnum(name) {
44585
45027
  }
44586
45028
  return widgetEnum;
44587
45029
  }
44588
- function getWidgetTypeFromWidgetId(widgetId) {
44589
- const [widgetType, widgetNumber] = widgetId.split(" ");
44590
- if (!widgetNumber) {
44591
- throw new Error(`Incorrect format for widget ID: ${widgetId}`);
44592
- }
44593
- if (!widgetNameToEnum[widgetType]) {
44594
- throw new Error(`Unknown widget type (${widgetType}) in widget ID: ${widgetId}`);
44595
- }
44596
- return widgetType;
44597
- }
44598
-
44599
- let WidgetType = /*#__PURE__*/function (WidgetType) {
44600
- WidgetType["Categorizer"] = "categorizer";
44601
- WidgetType["CSProgram"] = "cs-program";
44602
- WidgetType["Definition"] = "definition";
44603
- WidgetType["Dropdown"] = "dropdown";
44604
- WidgetType["Explanation"] = "explanation";
44605
- WidgetType["Expression"] = "expression";
44606
- WidgetType["GradedGroup"] = "graded-group";
44607
- WidgetType["GradedGroupAnswerBar"] = "graded-group-answer-bar";
44608
- WidgetType["GradedGroupSet"] = "graded-group-set";
44609
- WidgetType["Grapher"] = "grapher";
44610
- WidgetType["Group"] = "group";
44611
- WidgetType["Iframe"] = "iframe";
44612
- WidgetType["Image"] = "image";
44613
- WidgetType["InputNumber"] = "input-number";
44614
- WidgetType["Interaction"] = "interaction";
44615
- WidgetType["InteractiveGraph"] = "interactive-graph";
44616
- WidgetType["InteractiveNumberLine"] = "interactive-number-line";
44617
- WidgetType["LabelImage"] = "label-image";
44618
- WidgetType["Matcher"] = "matcher";
44619
- WidgetType["Matrix"] = "matrix";
44620
- WidgetType["Measurer"] = "measurer";
44621
- WidgetType["Molecule"] = "molecule";
44622
- WidgetType["NumberLine"] = "number-line";
44623
- WidgetType["NumericInput"] = "numeric-input";
44624
- WidgetType["Orderer"] = "orderer";
44625
- WidgetType["Passage"] = "passage";
44626
- WidgetType["PassageRef"] = "passage-ref";
44627
- WidgetType["PassageRefTarget"] = "passage-ref-target";
44628
- WidgetType["Plotter"] = "plotter";
44629
- WidgetType["Protractor"] = "protractor";
44630
- WidgetType["PythonProgram"] = "python-program";
44631
- WidgetType["Radio"] = "radio";
44632
- WidgetType["Sorter"] = "sorter";
44633
- WidgetType["Table"] = "table";
44634
- WidgetType["Unit"] = "unit";
44635
- WidgetType["Video"] = "video";
44636
- WidgetType["VideoTranscriptLink"] = "video-transcript-link";
44637
- return WidgetType;
44638
- }({});
44639
- // These are all the current widgets that require user-input and are supported for automatic scoring
44640
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
44641
-
44642
- /**
44643
- * Regex for widget placeholders in a string.
44644
- *
44645
- * First capture group is the widget ID (ex. 'radio 1')
44646
- * Second capture group is the widget type (ex. "radio)
44647
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
44648
- */
44649
- function getWidgetRegex() {
44650
- return /\[\[☃ (([a-z-]+) \d+)\]\]/g;
44651
- }
44652
-
44653
- /**
44654
- * Add a widget placeholder using the provided widget type and instance number.
44655
- * ex. addWidget(WidgetType.Radio, 1) => "[[☃ radio 1]]"
44656
- *
44657
- * @param {WidgetType} widgetType
44658
- * @param {number} instance
44659
- * @returns {string}
44660
- */
44661
- function addWidget(widgetType, instance) {
44662
- return `[[☃ ${widgetType} ${String(instance)}]]`;
44663
- }
44664
-
44665
- /**
44666
- * Extract all widget IDs, which includes the widget type and instance number.
44667
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
44668
- *
44669
- * Content should contain Perseus widget placeholders,
44670
- * which look like: '[[☃ radio 1]]'.
44671
- *
44672
- * @param {string} content
44673
- * @returns {Array<WidgetId>} widgets
44674
- */
44675
- function getAllWidgetIds(content) {
44676
- const widgets = [];
44677
- const localWidgetRegex = getWidgetRegex();
44678
- let match = localWidgetRegex.exec(content);
44679
- while (match !== null) {
44680
- widgets.push(match[1]);
44681
- match = localWidgetRegex.exec(content);
44682
- }
44683
- return widgets;
44684
- }
44685
-
44686
- /**
44687
- * Check if a specific widget is an actual widget type using its name.
44688
- * The widget name should include a widget type, without an instance number,
44689
- * as a string. ex. 'radio'
44690
- *
44691
- * @param {string} widgetName
44692
- * @returns {boolean}
44693
- */
44694
- function isWidgetType(widgetName) {
44695
- return Object.values(WidgetType).includes(widgetName);
44696
- }
44697
-
44698
- /**
44699
- * Extract all widget types from a Perseus JSON content string.
44700
- * This does not include the instance number and prevents duplicates.
44701
- * example output: ['radio', 'categorizer']
44702
- *
44703
- * @param {string} content
44704
- * @returns {Array<WidgetId>}
44705
- */
44706
- function getAllWidgetTypes(content) {
44707
- const widgetTypes = [];
44708
- const localWidgetRegex = getWidgetRegex();
44709
- let match = localWidgetRegex.exec(content);
44710
- while (match !== null) {
44711
- const matchType = match[2];
44712
- if (!widgetTypes.includes(matchType) && isWidgetType(matchType)) {
44713
- widgetTypes.push(matchType);
44714
- }
44715
- match = localWidgetRegex.exec(content);
44716
- }
44717
- return widgetTypes;
44718
- }
44719
-
44720
- /**
44721
- * Check if a specific widget is a question widget type using its ID.
44722
- * The widget ID includes the widget type and the instance number.
44723
- *
44724
- * @param {WidgetId} widgetId
44725
- * @returns {boolean}
44726
- */
44727
- function isQuestionWidgetType(widgetId) {
44728
- return QUESTION_WIDGETS.includes(widgetId.split(" ")[0]);
44729
- }
44730
-
44731
- /**
44732
- * Extract the widget IDs of all widgets considered question widgets. Widget IDs
44733
- * include widget type and instance number.
44734
- * example output: ['radio 1', 'categorizer 1']
44735
- *
44736
- * Content should contain Perseus widget placeholders,
44737
- * which look like: '[[☃ radio 1]]'.
44738
- *
44739
- * @param {string} content
44740
- * @returns Array<WidgetId>
44741
- */
44742
- function getQuestionWidgetIds(content) {
44743
- return getAllWidgetIds(content).filter(isQuestionWidgetType);
44744
- }
44745
45030
 
44746
45031
  const EditorJsonify = {
44747
45032
  serialize: function () {
@@ -44751,5 +45036,5 @@ const EditorJsonify = {
44751
45036
  }
44752
45037
  };
44753
45038
 
44754
- export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, WidgetType, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, convertWidgetNameToEnum, displaySigFigs, generateTestPerseusItem, genericPerseusItemData, getAllWidgetIds, getAllWidgetTypes, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetKey, getQuestionWidgetIds, getValidWidgetKeys, getWidgetTypeFromWidgetId, getWidgetTypeFromWidgetKey, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetKeyInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, violatingWidgets, allWidgets as widgets };
45039
+ export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, violatingWidgets, allWidgets as widgets };
44755
45040
  //# sourceMappingURL=index.js.map