@khanacademy/perseus 45.0.0 → 45.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/components/text-input.d.ts +1 -0
- package/dist/es/index.css +169 -12
- package/dist/es/index.js +249 -106
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +169 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +252 -106
- package/dist/index.js.map +1 -1
- package/dist/interactive2/objective_.d.ts +27 -6
- package/dist/perseus-types.d.ts +123 -72
- package/dist/renderer.d.ts +7 -1
- package/dist/types.d.ts +58 -16
- package/dist/util/parse-perseus-json/general-purpose-parsers/string-to-number.d.ts +2 -0
- package/dist/validation.types.d.ts +8 -3
- package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
- package/dist/widgets/group/group.d.ts +2 -0
- package/dist/widgets/group/score-group.d.ts +5 -0
- package/dist/widgets/interactive-graph.d.ts +96 -3
- package/dist/widgets/interactive-graphs/mafs-graph.d.ts +2 -2
- package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +3 -3
- package/dist/widgets/number-line/score-number-line.d.ts +2 -2
- package/dist/widgets/number-line/validate-number-line.d.ts +12 -0
- package/dist/widgets/numeric-input/numeric-input.d.ts +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3918,6 +3918,7 @@ Zoom.prototype.zoomImage = function () {
|
|
|
3918
3918
|
this._zoomOriginal();
|
|
3919
3919
|
}.bind(this);
|
|
3920
3920
|
img.src = this._targetImage.src;
|
|
3921
|
+
img.alt = this._targetImage.alt;
|
|
3921
3922
|
this.$zoomedImage = $zoomedImage;
|
|
3922
3923
|
};
|
|
3923
3924
|
Zoom.prototype._zoomOriginal = function () {
|
|
@@ -8993,7 +8994,10 @@ var Tex$1 = {
|
|
|
8993
8994
|
// We use createElement instead of JSX here because we can't name this file tex.tsx;
|
|
8994
8995
|
// that name is already taken.
|
|
8995
8996
|
// eslint-disable-next-line import/no-deprecated
|
|
8996
|
-
render(
|
|
8997
|
+
render(
|
|
8998
|
+
/*#__PURE__*/
|
|
8999
|
+
// eslint-disable-next-line react/no-children-prop
|
|
9000
|
+
React__namespace.createElement(TeX, {
|
|
8997
9001
|
children: text,
|
|
8998
9002
|
onRender: callback
|
|
8999
9003
|
}), $texHolder[0]);
|
|
@@ -14757,7 +14761,7 @@ const pluck = function (table, subKey) {
|
|
|
14757
14761
|
* });
|
|
14758
14762
|
* {a: 2, b: 3}
|
|
14759
14763
|
*/
|
|
14760
|
-
const mapObject
|
|
14764
|
+
const mapObject = function (obj, lambda) {
|
|
14761
14765
|
const result = {};
|
|
14762
14766
|
Object.keys(obj).forEach(key => {
|
|
14763
14767
|
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
@@ -14766,27 +14770,6 @@ const mapObject$2 = function (obj, lambda) {
|
|
|
14766
14770
|
return result;
|
|
14767
14771
|
};
|
|
14768
14772
|
|
|
14769
|
-
/**
|
|
14770
|
-
* Maps an array to an object
|
|
14771
|
-
*
|
|
14772
|
-
* > mapObjectFromArray(['a', 'b'], function(elem) {
|
|
14773
|
-
* return elem + elem;
|
|
14774
|
-
* });
|
|
14775
|
-
* {a: 'aa', b: 'bb'}
|
|
14776
|
-
*/
|
|
14777
|
-
const mapObjectFromArray = function (arr, lambda) {
|
|
14778
|
-
const result = {};
|
|
14779
|
-
arr.forEach(elem => {
|
|
14780
|
-
result[elem] = lambda(elem);
|
|
14781
|
-
});
|
|
14782
|
-
return result;
|
|
14783
|
-
};
|
|
14784
|
-
var objective_ = {
|
|
14785
|
-
pluck: pluck,
|
|
14786
|
-
mapObject: mapObject$2,
|
|
14787
|
-
mapObjectFromArray: mapObjectFromArray
|
|
14788
|
-
};
|
|
14789
|
-
|
|
14790
14773
|
/**
|
|
14791
14774
|
* MovableLine
|
|
14792
14775
|
*/
|
|
@@ -14851,7 +14834,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
|
|
|
14851
14834
|
// - are objects, not primitives (and need a deeper copy)
|
|
14852
14835
|
// - they don't need getters created for them
|
|
14853
14836
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
14854
|
-
|
|
14837
|
+
pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
|
|
14855
14838
|
},
|
|
14856
14839
|
/**
|
|
14857
14840
|
* Resets the object to its state as if it were constructed with
|
|
@@ -15358,7 +15341,7 @@ class MovablePoint$5 {
|
|
|
15358
15341
|
// - are objects, not primitives (and need a deeper copy)
|
|
15359
15342
|
// - they don't need getters created for them
|
|
15360
15343
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
15361
|
-
|
|
15344
|
+
pluck(MovablePointOptions, "standard")
|
|
15362
15345
|
// We only update props here, because we want things on state to
|
|
15363
15346
|
// be persistent, and updated appropriately in modify()
|
|
15364
15347
|
),
|
|
@@ -16259,7 +16242,7 @@ ___default["default"].extend(MovablePolygon.prototype, {
|
|
|
16259
16242
|
// - are objects, not primitives (and need a deeper copy)
|
|
16260
16243
|
// - they don't need getters created for them
|
|
16261
16244
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
16262
|
-
|
|
16245
|
+
pluck(MovablePolygonOptions, "standard")
|
|
16263
16246
|
|
|
16264
16247
|
// We only update props here, because we want things on state to
|
|
16265
16248
|
// be persistent, and updated appropriately in modify()
|
|
@@ -19481,12 +19464,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19481
19464
|
isLintable: isLintable
|
|
19482
19465
|
});
|
|
19483
19466
|
|
|
19484
|
-
const {
|
|
19485
|
-
mapObject: mapObject$1
|
|
19486
|
-
} = objective_;
|
|
19487
19467
|
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
19488
19468
|
// @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
|
|
19489
|
-
return mapObject
|
|
19469
|
+
return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
19490
19470
|
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19491
19471
|
const newValues = {};
|
|
19492
19472
|
if (!widgetInfo.type) {
|
|
@@ -19517,15 +19497,8 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19517
19497
|
// Static widgets shouldn't count as empty
|
|
19518
19498
|
return false;
|
|
19519
19499
|
}
|
|
19520
|
-
let score = null;
|
|
19521
|
-
const userInput = userInputMap[id];
|
|
19522
19500
|
const scorer = getWidgetScorer(widget.type);
|
|
19523
|
-
|
|
19524
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
|
|
19525
|
-
score = Util.flattenScores(scores);
|
|
19526
|
-
} else if (scorer) {
|
|
19527
|
-
score = scorer(userInput, widget.options, strings, locale);
|
|
19528
|
-
}
|
|
19501
|
+
const score = scorer?.(userInputMap[id], widget.options, strings, locale);
|
|
19529
19502
|
if (score) {
|
|
19530
19503
|
return Util.scoreIsEmpty(score);
|
|
19531
19504
|
}
|
|
@@ -19565,11 +19538,9 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19565
19538
|
}
|
|
19566
19539
|
const userInput = userInputMap[id];
|
|
19567
19540
|
const scorer = getWidgetScorer(widget.type);
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
widgetScores[id] =
|
|
19571
|
-
} else if (scorer) {
|
|
19572
|
-
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
19541
|
+
const score = scorer?.(userInput, widget.options, strings, locale);
|
|
19542
|
+
if (score != null) {
|
|
19543
|
+
widgetScores[id] = score;
|
|
19573
19544
|
}
|
|
19574
19545
|
});
|
|
19575
19546
|
return widgetScores;
|
|
@@ -19876,9 +19847,6 @@ class WidgetContainer extends React__namespace.Component {
|
|
|
19876
19847
|
}
|
|
19877
19848
|
}
|
|
19878
19849
|
|
|
19879
|
-
const {
|
|
19880
|
-
mapObject
|
|
19881
|
-
} = objective_;
|
|
19882
19850
|
const rContainsNonWhitespace = /\S/;
|
|
19883
19851
|
const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
|
|
19884
19852
|
const noopOnRender = () => {};
|
|
@@ -20995,6 +20963,13 @@ class Renderer extends React__namespace.Component {
|
|
|
20995
20963
|
}, this);
|
|
20996
20964
|
return state;
|
|
20997
20965
|
};
|
|
20966
|
+
|
|
20967
|
+
/**
|
|
20968
|
+
* Returns an array of widget ids that are empty (meaning widgets where the
|
|
20969
|
+
* learner has not interacted with the widget yet or has not filled in all
|
|
20970
|
+
* fields). For example, the `interactive-graph` widget is considered
|
|
20971
|
+
* empty if the graph is in the starting state.
|
|
20972
|
+
*/
|
|
20998
20973
|
emptyWidgets() {
|
|
20999
20974
|
return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
21000
20975
|
}
|
|
@@ -21048,7 +21023,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21048
21023
|
}, 0);
|
|
21049
21024
|
});
|
|
21050
21025
|
};
|
|
21051
|
-
setInputValue = (path, newValue,
|
|
21026
|
+
setInputValue = (path, newValue, cb) => {
|
|
21052
21027
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
21053
21028
|
const widgetId = ___default["default"].first(path);
|
|
21054
21029
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
@@ -21056,7 +21031,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21056
21031
|
const widget = this.getWidgetInstance(widgetId);
|
|
21057
21032
|
|
|
21058
21033
|
// Widget handles parsing of the interWidgetPath.
|
|
21059
|
-
widget?.setInputValue?.(interWidgetPath, newValue,
|
|
21034
|
+
widget?.setInputValue?.(interWidgetPath, newValue, cb);
|
|
21060
21035
|
};
|
|
21061
21036
|
|
|
21062
21037
|
/**
|
|
@@ -21354,6 +21329,7 @@ class TextInput extends React__namespace.Component {
|
|
|
21354
21329
|
value: formattedValue,
|
|
21355
21330
|
type: "text",
|
|
21356
21331
|
"aria-label": labelText,
|
|
21332
|
+
"aria-describedby": this.props["aria-describedby"],
|
|
21357
21333
|
onChange: value => this.props.onChange(value),
|
|
21358
21334
|
placeholder: placeholder,
|
|
21359
21335
|
onFocus: onFocus,
|
|
@@ -21612,6 +21588,7 @@ class Tooltip extends React__namespace.Component {
|
|
|
21612
21588
|
}, /*#__PURE__*/React__namespace.createElement("div", {
|
|
21613
21589
|
ref: this.tooltipContainerRef,
|
|
21614
21590
|
className: "tooltipContainer",
|
|
21591
|
+
role: "tooltip",
|
|
21615
21592
|
style: {
|
|
21616
21593
|
position: "absolute",
|
|
21617
21594
|
// height must start out undefined, not null, so that
|
|
@@ -21688,9 +21665,15 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21688
21665
|
};
|
|
21689
21666
|
_renderInput = () => {
|
|
21690
21667
|
const id = this._getUniqueId();
|
|
21668
|
+
const ariaId = `aria-for-${id}`;
|
|
21669
|
+
// Generate text from a known set of format options that will read well in a screen reader
|
|
21670
|
+
const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
|
|
21671
|
+
${this.props.examples.slice(1).join(", or\n")}`
|
|
21672
|
+
// @ts-expect-error TS2550: Property replaceAll does not exist on type string.
|
|
21673
|
+
.replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
|
|
21691
21674
|
const inputProps = {
|
|
21692
21675
|
id: id,
|
|
21693
|
-
"aria-describedby":
|
|
21676
|
+
"aria-describedby": ariaId,
|
|
21694
21677
|
ref: "input",
|
|
21695
21678
|
className: this._getInputClassName(),
|
|
21696
21679
|
labelText: this.props.labelText,
|
|
@@ -21706,7 +21689,12 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21706
21689
|
autoCorrect: "off",
|
|
21707
21690
|
spellCheck: "false"
|
|
21708
21691
|
};
|
|
21709
|
-
return /*#__PURE__*/React__namespace.createElement(TextInput, inputProps)
|
|
21692
|
+
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(TextInput, inputProps), /*#__PURE__*/React__namespace.createElement("span", {
|
|
21693
|
+
id: ariaId,
|
|
21694
|
+
style: {
|
|
21695
|
+
display: "none"
|
|
21696
|
+
}
|
|
21697
|
+
}, examplesAria));
|
|
21710
21698
|
};
|
|
21711
21699
|
_handleFocus = () => {
|
|
21712
21700
|
this.props.onFocus();
|
|
@@ -21747,8 +21735,12 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21747
21735
|
};
|
|
21748
21736
|
render() {
|
|
21749
21737
|
const input = this._renderInput();
|
|
21750
|
-
const examplesContent =
|
|
21751
|
-
|
|
21738
|
+
const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
|
|
21739
|
+
: this.props.examples // 2 or more items should display as a list
|
|
21740
|
+
.map((example, index) => {
|
|
21741
|
+
// If the first example is bold, then it is most likely a heading/leading text.
|
|
21742
|
+
// So, it shouldn't be part of the list.
|
|
21743
|
+
return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
|
|
21752
21744
|
}).join("\n");
|
|
21753
21745
|
const showExamples = this.props.shouldShowExamples && this.state.showExamples;
|
|
21754
21746
|
return /*#__PURE__*/React__namespace.createElement(Tooltip, {
|
|
@@ -22402,7 +22394,7 @@ function scoreNumericInput(userInput, rubric, strings) {
|
|
|
22402
22394
|
// We may have received TeX; try to parse it before grading.
|
|
22403
22395
|
// If `currentValue` is not TeX, this should be a no-op.
|
|
22404
22396
|
const currentValue = ParseTex(userInput.currentValue);
|
|
22405
|
-
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
|
|
22397
|
+
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
|
|
22406
22398
|
|
|
22407
22399
|
// The coefficient is an attribute of the widget
|
|
22408
22400
|
let localValue = currentValue;
|
|
@@ -22478,7 +22470,8 @@ class NumericInput extends React__namespace.Component {
|
|
|
22478
22470
|
state = {
|
|
22479
22471
|
// keeps track of the other set of values when switching
|
|
22480
22472
|
// between 0 and finite solutions
|
|
22481
|
-
previousValues: [""]
|
|
22473
|
+
previousValues: [""],
|
|
22474
|
+
isFocused: false
|
|
22482
22475
|
};
|
|
22483
22476
|
|
|
22484
22477
|
// TODO(Nicole, Jeremy): This is maybe never used and should be removed
|
|
@@ -22543,9 +22536,21 @@ class NumericInput extends React__namespace.Component {
|
|
|
22543
22536
|
};
|
|
22544
22537
|
_handleFocus = () => {
|
|
22545
22538
|
this.props.onFocus([]);
|
|
22539
|
+
this.setState(currentState => {
|
|
22540
|
+
return {
|
|
22541
|
+
...currentState,
|
|
22542
|
+
isFocused: true
|
|
22543
|
+
};
|
|
22544
|
+
});
|
|
22546
22545
|
};
|
|
22547
22546
|
_handleBlur = () => {
|
|
22548
22547
|
this.props.onBlur([]);
|
|
22548
|
+
this.setState(currentState => {
|
|
22549
|
+
return {
|
|
22550
|
+
...currentState,
|
|
22551
|
+
isFocused: false
|
|
22552
|
+
};
|
|
22553
|
+
});
|
|
22549
22554
|
};
|
|
22550
22555
|
render() {
|
|
22551
22556
|
let labelText = this.props.labelText;
|
|
@@ -22576,13 +22581,20 @@ class NumericInput extends React__namespace.Component {
|
|
|
22576
22581
|
// component.
|
|
22577
22582
|
const styles = aphrodite.StyleSheet.create({
|
|
22578
22583
|
input: {
|
|
22584
|
+
borderRadius: "3px",
|
|
22585
|
+
borderWidth: this.state.isFocused ? "2px" : "1px",
|
|
22586
|
+
display: "inline-block",
|
|
22587
|
+
fontFamily: `Symbola, "Times New Roman", serif`,
|
|
22588
|
+
fontSize: "18px",
|
|
22589
|
+
height: "32px",
|
|
22590
|
+
lineHeight: "18px",
|
|
22591
|
+
padding: this.state.isFocused ? "4px" : "4px 5px",
|
|
22592
|
+
// account for added focus border thickness
|
|
22579
22593
|
textAlign: this.props.rightAlign ? "right" : "left",
|
|
22580
|
-
width: this.props.size === "small" ? 40 : 80
|
|
22581
|
-
padding: 0,
|
|
22582
|
-
height: "auto"
|
|
22594
|
+
width: this.props.size === "small" ? 40 : 80
|
|
22583
22595
|
}
|
|
22584
22596
|
});
|
|
22585
|
-
return /*#__PURE__*/React__namespace.createElement(
|
|
22597
|
+
return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
|
|
22586
22598
|
ref: ref => this.inputRef = ref,
|
|
22587
22599
|
value: this.props.currentValue,
|
|
22588
22600
|
onChange: this.handleChange,
|
|
@@ -22594,7 +22606,7 @@ class NumericInput extends React__namespace.Component {
|
|
|
22594
22606
|
id: this.props.widgetId,
|
|
22595
22607
|
disabled: this.props.apiOptions.readOnly,
|
|
22596
22608
|
style: styles.input
|
|
22597
|
-
})
|
|
22609
|
+
});
|
|
22598
22610
|
}
|
|
22599
22611
|
}
|
|
22600
22612
|
|
|
@@ -27522,8 +27534,8 @@ var Definition$1 = {
|
|
|
27522
27534
|
scorer: () => scoreNoop()
|
|
27523
27535
|
};
|
|
27524
27536
|
|
|
27525
|
-
//
|
|
27526
|
-
//
|
|
27537
|
+
// There are no required props, but this component can receive props from any
|
|
27538
|
+
// deprecated widget
|
|
27527
27539
|
class DeprecatedStandin extends React__default["default"].Component {
|
|
27528
27540
|
static contextType = PerseusI18nContext;
|
|
27529
27541
|
// this just helps with TS weak typing when a Widget
|
|
@@ -29646,6 +29658,13 @@ const getPromptJSON$f = rendererJSON => {
|
|
|
29646
29658
|
};
|
|
29647
29659
|
};
|
|
29648
29660
|
|
|
29661
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
29662
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
29663
|
+
function scoreGroup(userInput, options, strings, locale) {
|
|
29664
|
+
const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
|
|
29665
|
+
return Util.flattenScores(scores);
|
|
29666
|
+
}
|
|
29667
|
+
|
|
29649
29668
|
// exports has no 'transform'
|
|
29650
29669
|
|
|
29651
29670
|
class Group extends React__namespace.Component {
|
|
@@ -29797,6 +29816,9 @@ var Group$1 = {
|
|
|
29797
29816
|
displayName: "Group (SAT only)",
|
|
29798
29817
|
widget: Group,
|
|
29799
29818
|
traverseChildWidgets: traverseChildWidgets,
|
|
29819
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29820
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
|
|
29821
|
+
scorer: scoreGroup,
|
|
29800
29822
|
hidden: true,
|
|
29801
29823
|
isLintable: true
|
|
29802
29824
|
};
|
|
@@ -32540,6 +32562,61 @@ const LegacyGrid = _ref => {
|
|
|
32540
32562
|
|
|
32541
32563
|
// Range is replaced within this file with Interval, but it is used elsewhere
|
|
32542
32564
|
// and exported from the package, so we need to keep it around.
|
|
32565
|
+
/**
|
|
32566
|
+
* Our core set of Perseus widgets.
|
|
32567
|
+
*
|
|
32568
|
+
* This interface is the basis for "registering" all Perseus widget types.
|
|
32569
|
+
* There should be one key/value pair for each supported widget. If you create
|
|
32570
|
+
* a new widget, an entry should be added to this interface. Note that this
|
|
32571
|
+
* only registers the widget options type, you'll also need to register the
|
|
32572
|
+
* widget so that it's available at runtime (@see
|
|
32573
|
+
* {@link file://./widgets.ts#registerWidget}).
|
|
32574
|
+
*
|
|
32575
|
+
* Importantly, the key should be the name that is used in widget IDs. For most
|
|
32576
|
+
* widgets that is the same as the widget option's `type` field. In cases where
|
|
32577
|
+
* a widget has been deprecated and replaced with the deprecated-standin
|
|
32578
|
+
* widget, it should be the original widget type!
|
|
32579
|
+
*
|
|
32580
|
+
* If you define the widget outside of this package, you can still add the new
|
|
32581
|
+
* widget to this interface by writing the following in that package that
|
|
32582
|
+
* contains the widget. TypeScript will merge that definition of the
|
|
32583
|
+
* `PerseusWidgets` with the one defined below.
|
|
32584
|
+
*
|
|
32585
|
+
* ```typescript
|
|
32586
|
+
* declare module "@khanacademy/perseus" {
|
|
32587
|
+
* interface PerseusWidgetTypes {
|
|
32588
|
+
* // A new widget
|
|
32589
|
+
* "new-awesomeness": MyAwesomeNewWidget;
|
|
32590
|
+
*
|
|
32591
|
+
* // A deprecated widget
|
|
32592
|
+
* "super-old-widget": AutoCorrectWidget;
|
|
32593
|
+
* }
|
|
32594
|
+
* }
|
|
32595
|
+
*
|
|
32596
|
+
* // The new widget's options definition
|
|
32597
|
+
* type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
|
|
32598
|
+
*
|
|
32599
|
+
* // The deprecated widget's options definition
|
|
32600
|
+
* type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
|
|
32601
|
+
* ```
|
|
32602
|
+
*
|
|
32603
|
+
* This interface can be extended through the magic of TypeScript "Declaration
|
|
32604
|
+
* merging". Specifically, we augment this module and extend this interface.
|
|
32605
|
+
*
|
|
32606
|
+
* @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
|
|
32607
|
+
*/
|
|
32608
|
+
/**
|
|
32609
|
+
* A map of widget IDs to widget options. This is most often used as the type
|
|
32610
|
+
* for a set of widgets defined in a `PerseusItem` but can also be useful to
|
|
32611
|
+
* represent a function parameter where only `widgets` from a `PerseusItem` are
|
|
32612
|
+
* needed. Today Widget IDs are made up of the widget type and an incrementing
|
|
32613
|
+
* integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
|
|
32614
|
+
* reading/parsing the widget id to derive any information from it, except in
|
|
32615
|
+
* the case of this map.
|
|
32616
|
+
*
|
|
32617
|
+
* @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
|
|
32618
|
+
* by augmenting the PerseusWidgetTypes with new widget types!
|
|
32619
|
+
*/
|
|
32543
32620
|
/**
|
|
32544
32621
|
* A "PerseusItem" is a classic Perseus item. It is rendered by the
|
|
32545
32622
|
* `ServerItemRenderer` and the layout is pre-set.
|
|
@@ -32555,6 +32632,9 @@ const LegacyGrid = _ref => {
|
|
|
32555
32632
|
* A "MultiItem" is an advanced Perseus item. It is rendered by the
|
|
32556
32633
|
* `MultiRenderer` and you can control the layout of individual parts of the
|
|
32557
32634
|
* item.
|
|
32635
|
+
*
|
|
32636
|
+
* @deprecated MultiItem support is slated for removal in a future Perseus
|
|
32637
|
+
* release.
|
|
32558
32638
|
*/
|
|
32559
32639
|
const ItemExtras = [
|
|
32560
32640
|
// The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
|
|
@@ -32576,6 +32656,11 @@ const ItemExtras = [
|
|
|
32576
32656
|
// The user might benefit from using a statistics Z Table like https://www.ztable.net/
|
|
32577
32657
|
"zTable"];
|
|
32578
32658
|
|
|
32659
|
+
/**
|
|
32660
|
+
* The type representing the common structure of all widget's options. The
|
|
32661
|
+
* `Options` generic type represents the widget-specific option data.
|
|
32662
|
+
*/
|
|
32663
|
+
|
|
32579
32664
|
// prettier-ignore
|
|
32580
32665
|
|
|
32581
32666
|
// prettier-ignore
|
|
@@ -32644,7 +32729,9 @@ const ItemExtras = [
|
|
|
32644
32729
|
|
|
32645
32730
|
//prettier-ignore
|
|
32646
32731
|
|
|
32647
|
-
|
|
32732
|
+
/**
|
|
32733
|
+
* A background image applied to various widgets.
|
|
32734
|
+
*/
|
|
32648
32735
|
|
|
32649
32736
|
const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
|
|
32650
32737
|
|
|
@@ -37288,8 +37375,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
|
|
|
37288
37375
|
});
|
|
37289
37376
|
};
|
|
37290
37377
|
|
|
37291
|
-
// There's no transform function in exports
|
|
37292
|
-
|
|
37293
37378
|
// TODO: there's another, very similar getSinusoidCoefficients function
|
|
37294
37379
|
// they should probably be merged
|
|
37295
37380
|
function getSinusoidCoefficients(coords) {
|
|
@@ -42407,23 +42492,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
|
|
|
42407
42492
|
};
|
|
42408
42493
|
};
|
|
42409
42494
|
|
|
42410
|
-
|
|
42411
|
-
|
|
42412
|
-
|
|
42413
|
-
|
|
42414
|
-
|
|
42415
|
-
|
|
42416
|
-
|
|
42417
|
-
const
|
|
42495
|
+
/**
|
|
42496
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
42497
|
+
* state.
|
|
42498
|
+
* @param userInput
|
|
42499
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
42500
|
+
*/
|
|
42501
|
+
function validateNumberLine(userInput) {
|
|
42502
|
+
const divisionRange = userInput.divisionRange;
|
|
42503
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
42418
42504
|
|
|
42419
42505
|
// TODO: I don't think isTickCrtl is a thing anymore
|
|
42420
|
-
if (
|
|
42506
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
42421
42507
|
return {
|
|
42422
42508
|
type: "invalid",
|
|
42423
42509
|
message: "Number of divisions is outside the allowed range."
|
|
42424
42510
|
};
|
|
42425
42511
|
}
|
|
42426
|
-
|
|
42512
|
+
return null;
|
|
42513
|
+
}
|
|
42514
|
+
|
|
42515
|
+
function scoreNumberLine(userInput, scoringData) {
|
|
42516
|
+
const validationError = validateNumberLine(userInput);
|
|
42517
|
+
if (validationError) {
|
|
42518
|
+
return validationError;
|
|
42519
|
+
}
|
|
42520
|
+
const range = scoringData.range;
|
|
42521
|
+
const start = scoringData.initialX != null ? scoringData.initialX : range[0];
|
|
42522
|
+
const startRel = scoringData.isInequality ? "ge" : "eq";
|
|
42523
|
+
const correctRel = scoringData.correctRel || "eq";
|
|
42524
|
+
const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
|
|
42525
|
+
if (correctPos && correctRel === userInput.rel) {
|
|
42427
42526
|
return {
|
|
42428
42527
|
type: "points",
|
|
42429
42528
|
earned: 1,
|
|
@@ -42431,7 +42530,7 @@ function scoreNumberLine(state, rubric) {
|
|
|
42431
42530
|
message: null
|
|
42432
42531
|
};
|
|
42433
42532
|
}
|
|
42434
|
-
if (
|
|
42533
|
+
if (userInput.numLinePosition === start && userInput.rel === startRel) {
|
|
42435
42534
|
// We're where we started.
|
|
42436
42535
|
return {
|
|
42437
42536
|
type: "invalid",
|
|
@@ -45380,7 +45479,7 @@ const init = function () {
|
|
|
45380
45479
|
|
|
45381
45480
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45382
45481
|
const libName = "@khanacademy/perseus";
|
|
45383
|
-
const libVersion = "45.
|
|
45482
|
+
const libVersion = "45.1.0";
|
|
45384
45483
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45385
45484
|
|
|
45386
45485
|
const apiVersion = {
|
|
@@ -45968,14 +46067,10 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45968
46067
|
});
|
|
45969
46068
|
}
|
|
45970
46069
|
componentDidUpdate(prevProps, prevState) {
|
|
45971
|
-
|
|
46070
|
+
const answerableCallback = this.props.apiOptions.answerableCallback;
|
|
46071
|
+
if (answerableCallback != null) {
|
|
45972
46072
|
const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
|
|
45973
|
-
|
|
45974
|
-
answerableCallback
|
|
45975
|
-
} = this.props.apiOptions;
|
|
45976
|
-
if (answerableCallback) {
|
|
45977
|
-
answerableCallback(isAnswerable);
|
|
45978
|
-
}
|
|
46073
|
+
answerableCallback(isAnswerable);
|
|
45979
46074
|
}
|
|
45980
46075
|
if (!this._fullyRendered) {
|
|
45981
46076
|
const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
|
|
@@ -46346,7 +46441,7 @@ const traverseRenderer$1 = function (rendererOptions, contentCallback, deepWidge
|
|
|
46346
46441
|
newContent = modifiedContent;
|
|
46347
46442
|
}
|
|
46348
46443
|
}
|
|
46349
|
-
const newWidgets =
|
|
46444
|
+
const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
|
|
46350
46445
|
// Widgets without info or a type are empty widgets, and
|
|
46351
46446
|
// should always be renderable. It's also annoying to write
|
|
46352
46447
|
// checks for this everywhere, so we just filter them out once and
|
|
@@ -48956,7 +49051,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
|
|
|
48956
49051
|
const parseDefinitionWidget = parseWidget(constant("definition"), object({
|
|
48957
49052
|
togglePrompt: string,
|
|
48958
49053
|
definition: string,
|
|
48959
|
-
static: boolean
|
|
49054
|
+
static: defaulted(boolean, () => false)
|
|
48960
49055
|
}));
|
|
48961
49056
|
|
|
48962
49057
|
const parseDropdownWidget = parseWidget(constant("dropdown"), object({
|
|
@@ -49159,15 +49254,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
|
|
|
49159
49254
|
static: defaulted(boolean, () => false)
|
|
49160
49255
|
}));
|
|
49161
49256
|
|
|
49257
|
+
const stringToNumber = (rawValue, ctx) => {
|
|
49258
|
+
if (typeof rawValue === "number") {
|
|
49259
|
+
return ctx.success(rawValue);
|
|
49260
|
+
}
|
|
49261
|
+
const parsedNumber = +rawValue;
|
|
49262
|
+
if (rawValue === "" || isNaN(parsedNumber)) {
|
|
49263
|
+
return ctx.failure("a number or numeric string", rawValue);
|
|
49264
|
+
}
|
|
49265
|
+
return ctx.success(parsedNumber);
|
|
49266
|
+
};
|
|
49267
|
+
|
|
49268
|
+
const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
|
|
49162
49269
|
const parsePerseusImageBackground = object({
|
|
49163
49270
|
url: optional(nullable(string)),
|
|
49164
|
-
width: optional(
|
|
49165
|
-
height: optional(
|
|
49166
|
-
top: optional(
|
|
49167
|
-
left: optional(
|
|
49168
|
-
bottom: optional(
|
|
49169
|
-
|
|
49170
|
-
scale: optional(union(number).or(string).parser)
|
|
49271
|
+
width: optional(numericToNumber),
|
|
49272
|
+
height: optional(numericToNumber),
|
|
49273
|
+
top: optional(numericToNumber),
|
|
49274
|
+
left: optional(numericToNumber),
|
|
49275
|
+
bottom: optional(numericToNumber),
|
|
49276
|
+
scale: optional(numericToNumber)
|
|
49171
49277
|
});
|
|
49172
49278
|
|
|
49173
49279
|
const pairOfNumbers$2 = pair(number, number);
|
|
@@ -49186,6 +49292,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
|
|
|
49186
49292
|
box: optional(pairOfNumbers$2)
|
|
49187
49293
|
}));
|
|
49188
49294
|
|
|
49295
|
+
const booleanToString = (rawValue, ctx) => {
|
|
49296
|
+
if (typeof rawValue === "boolean") {
|
|
49297
|
+
return ctx.success(String(rawValue));
|
|
49298
|
+
}
|
|
49299
|
+
return ctx.failure("boolean", rawValue);
|
|
49300
|
+
};
|
|
49189
49301
|
const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
49190
49302
|
answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
|
|
49191
49303
|
inexact: optional(boolean),
|
|
@@ -49193,7 +49305,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
|
49193
49305
|
rightAlign: optional(boolean),
|
|
49194
49306
|
simplify: enumeration("required", "optional", "enforced"),
|
|
49195
49307
|
size: enumeration("normal", "small"),
|
|
49196
|
-
|
|
49308
|
+
// TODO(benchristel): there are some content items where value is a
|
|
49309
|
+
// boolean, even though that makes no sense. We should figure out if
|
|
49310
|
+
// those content items are actually published anywhere, and consider
|
|
49311
|
+
// updating them.
|
|
49312
|
+
value: union(number).or(string).or(booleanToString).parser,
|
|
49197
49313
|
customKeypad: optional(boolean)
|
|
49198
49314
|
}));
|
|
49199
49315
|
|
|
@@ -49491,18 +49607,27 @@ const parseLockedFunctionType = object({
|
|
|
49491
49607
|
const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
|
|
49492
49608
|
const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
|
|
49493
49609
|
step: pairOfNumbers,
|
|
49494
|
-
|
|
49495
|
-
|
|
49610
|
+
// TODO(benchristel): rather than making gridStep and snapStep
|
|
49611
|
+
// optional, we should duplicate the defaulting logic from the
|
|
49612
|
+
// InteractiveGraph component. See parse-perseus-json/README.md for
|
|
49613
|
+
// why.
|
|
49614
|
+
gridStep: optional(pairOfNumbers),
|
|
49615
|
+
snapStep: optional(pairOfNumbers),
|
|
49496
49616
|
backgroundImage: optional(parsePerseusImageBackground),
|
|
49497
49617
|
markings: enumeration("graph", "grid", "none"),
|
|
49498
|
-
labels: array(string),
|
|
49618
|
+
labels: optional(array(string)),
|
|
49499
49619
|
showProtractor: boolean,
|
|
49500
49620
|
showRuler: optional(boolean),
|
|
49501
49621
|
showTooltips: optional(boolean),
|
|
49502
49622
|
rulerLabel: optional(string),
|
|
49503
49623
|
rulerTicks: optional(number),
|
|
49504
49624
|
range: pair(pairOfNumbers, pairOfNumbers),
|
|
49505
|
-
|
|
49625
|
+
// NOTE(benchristel): I copied the default graph from
|
|
49626
|
+
// interactive-graph.tsx. See the parse-perseus-json/README.md for
|
|
49627
|
+
// an explanation of why we want to duplicate the default here.
|
|
49628
|
+
graph: defaulted(parsePerseusGraphType, () => ({
|
|
49629
|
+
type: "linear"
|
|
49630
|
+
})),
|
|
49506
49631
|
correct: parsePerseusGraphType,
|
|
49507
49632
|
// TODO(benchristel): default lockedFigures to empty array
|
|
49508
49633
|
lockedFigures: optional(array(parseLockedFigure)),
|
|
@@ -49570,7 +49695,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
|
|
|
49570
49695
|
isTickCtrl: optional(nullable(boolean)),
|
|
49571
49696
|
divisionRange: array(number),
|
|
49572
49697
|
numDivisions: optional(nullable(number)),
|
|
49573
|
-
|
|
49698
|
+
// NOTE(benchristel): I copied the default snapDivisions from
|
|
49699
|
+
// number-line.tsx. See the parse-perseus-json/README.md for
|
|
49700
|
+
// an explanation of why we want to duplicate the default here.
|
|
49701
|
+
snapDivisions: defaulted(number, () => 2),
|
|
49574
49702
|
tickStep: optional(nullable(number)),
|
|
49575
49703
|
correctRel: optional(nullable(string)),
|
|
49576
49704
|
correctX: number,
|
|
@@ -49583,18 +49711,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
|
|
|
49583
49711
|
const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
49584
49712
|
answers: array(object({
|
|
49585
49713
|
message: string,
|
|
49586
|
-
value: number,
|
|
49714
|
+
value: optional(number),
|
|
49587
49715
|
status: string,
|
|
49588
49716
|
answerForms: optional(array(parseMathFormat)),
|
|
49589
49717
|
strict: boolean,
|
|
49590
49718
|
maxError: optional(nullable(number)),
|
|
49591
49719
|
simplify: optional(nullable(string))
|
|
49592
49720
|
})),
|
|
49593
|
-
labelText: string,
|
|
49721
|
+
labelText: optional(string),
|
|
49594
49722
|
size: string,
|
|
49595
49723
|
coefficient: boolean,
|
|
49596
49724
|
rightAlign: optional(boolean),
|
|
49597
|
-
static: boolean,
|
|
49725
|
+
static: defaulted(boolean, () => false),
|
|
49598
49726
|
answerForms: optional(array(object({
|
|
49599
49727
|
name: parseMathFormat,
|
|
49600
49728
|
simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
|
|
@@ -49607,12 +49735,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
|
49607
49735
|
function parseRenderer(rawValue, ctx) {
|
|
49608
49736
|
return parsePerseusRenderer(rawValue, ctx);
|
|
49609
49737
|
}
|
|
49738
|
+
const largeToAuto = (height, ctx) => {
|
|
49739
|
+
if (height === "large") {
|
|
49740
|
+
return ctx.success("auto");
|
|
49741
|
+
}
|
|
49742
|
+
return ctx.success(height);
|
|
49743
|
+
};
|
|
49610
49744
|
const parseOrdererWidget = parseWidget(constant("orderer"), object({
|
|
49611
|
-
options: array(parseRenderer),
|
|
49745
|
+
options: defaulted(array(parseRenderer), () => []),
|
|
49612
49746
|
correctOptions: array(parseRenderer),
|
|
49613
49747
|
otherOptions: array(parseRenderer),
|
|
49614
|
-
height: enumeration("normal", "auto"),
|
|
49615
|
-
layout: enumeration("horizontal", "vertical")
|
|
49748
|
+
height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
|
|
49749
|
+
layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
|
|
49616
49750
|
}));
|
|
49617
49751
|
|
|
49618
49752
|
const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
|
|
@@ -49626,7 +49760,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
|
|
|
49626
49760
|
passageText: string,
|
|
49627
49761
|
passageTitle: string,
|
|
49628
49762
|
showLineNumbers: boolean,
|
|
49629
|
-
static: boolean
|
|
49763
|
+
static: defaulted(boolean, () => false)
|
|
49630
49764
|
}));
|
|
49631
49765
|
|
|
49632
49766
|
const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
|
|
@@ -49647,7 +49781,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
|
|
|
49647
49781
|
picUrl: optional(nullable(string)),
|
|
49648
49782
|
picSize: optional(nullable(number)),
|
|
49649
49783
|
picBoxHeight: optional(nullable(number)),
|
|
49650
|
-
|
|
49784
|
+
// NOTE(benchristel): I copied the default plotDimensions from
|
|
49785
|
+
// plotter.tsx. See the parse-perseus-json/README.md for an explanation
|
|
49786
|
+
// of why we want to duplicate the defaults here.
|
|
49787
|
+
plotDimensions: defaulted(array(number), () => [380, 300])
|
|
49651
49788
|
}));
|
|
49652
49789
|
|
|
49653
49790
|
const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
|
|
@@ -49800,6 +49937,10 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
|
49800
49937
|
return parseAndAssign(`table ${id}`, parseTableWidget);
|
|
49801
49938
|
case "video":
|
|
49802
49939
|
return parseAndAssign(`video ${id}`, parseVideoWidget);
|
|
49940
|
+
case "sequence":
|
|
49941
|
+
// sequence is a deprecated widget type, and the corresponding
|
|
49942
|
+
// widget component no longer exists.
|
|
49943
|
+
return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
|
|
49803
49944
|
default:
|
|
49804
49945
|
if (getWidget(type)) {
|
|
49805
49946
|
// @ts-expect-error - 'type' is not a valid widget type
|
|
@@ -49808,6 +49949,11 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
|
49808
49949
|
return ctx.failure("a valid widget type", type);
|
|
49809
49950
|
}
|
|
49810
49951
|
};
|
|
49952
|
+
const parseDeprecatedWidget = parseWidget(
|
|
49953
|
+
// Ignore the incoming widget type and hardcode "deprecated-standin"
|
|
49954
|
+
(_, ctx) => ctx.success("deprecated-standin"),
|
|
49955
|
+
// Allow any widget options
|
|
49956
|
+
object({}));
|
|
49811
49957
|
const parseStringToPositiveInt = (rawValue, ctx) => {
|
|
49812
49958
|
if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
|
|
49813
49959
|
return ctx.failure("numeric string", rawValue);
|