@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/es/index.js
CHANGED
|
@@ -3797,6 +3797,7 @@ Zoom.prototype.zoomImage = function () {
|
|
|
3797
3797
|
this._zoomOriginal();
|
|
3798
3798
|
}.bind(this);
|
|
3799
3799
|
img.src = this._targetImage.src;
|
|
3800
|
+
img.alt = this._targetImage.alt;
|
|
3800
3801
|
this.$zoomedImage = $zoomedImage;
|
|
3801
3802
|
};
|
|
3802
3803
|
Zoom.prototype._zoomOriginal = function () {
|
|
@@ -8872,7 +8873,10 @@ var Tex$1 = {
|
|
|
8872
8873
|
// We use createElement instead of JSX here because we can't name this file tex.tsx;
|
|
8873
8874
|
// that name is already taken.
|
|
8874
8875
|
// eslint-disable-next-line import/no-deprecated
|
|
8875
|
-
render(
|
|
8876
|
+
render(
|
|
8877
|
+
/*#__PURE__*/
|
|
8878
|
+
// eslint-disable-next-line react/no-children-prop
|
|
8879
|
+
React.createElement(TeX, {
|
|
8876
8880
|
children: text,
|
|
8877
8881
|
onRender: callback
|
|
8878
8882
|
}), $texHolder[0]);
|
|
@@ -14609,7 +14613,7 @@ const pluck = function pluck(table, subKey) {
|
|
|
14609
14613
|
* });
|
|
14610
14614
|
* {a: 2, b: 3}
|
|
14611
14615
|
*/
|
|
14612
|
-
const mapObject
|
|
14616
|
+
const mapObject = function mapObject(obj, lambda) {
|
|
14613
14617
|
const result = {};
|
|
14614
14618
|
Object.keys(obj).forEach(key => {
|
|
14615
14619
|
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
@@ -14618,27 +14622,6 @@ const mapObject$2 = function mapObject(obj, lambda) {
|
|
|
14618
14622
|
return result;
|
|
14619
14623
|
};
|
|
14620
14624
|
|
|
14621
|
-
/**
|
|
14622
|
-
* Maps an array to an object
|
|
14623
|
-
*
|
|
14624
|
-
* > mapObjectFromArray(['a', 'b'], function(elem) {
|
|
14625
|
-
* return elem + elem;
|
|
14626
|
-
* });
|
|
14627
|
-
* {a: 'aa', b: 'bb'}
|
|
14628
|
-
*/
|
|
14629
|
-
const mapObjectFromArray = function mapObjectFromArray(arr, lambda) {
|
|
14630
|
-
const result = {};
|
|
14631
|
-
arr.forEach(elem => {
|
|
14632
|
-
result[elem] = lambda(elem);
|
|
14633
|
-
});
|
|
14634
|
-
return result;
|
|
14635
|
-
};
|
|
14636
|
-
var objective_ = {
|
|
14637
|
-
pluck: pluck,
|
|
14638
|
-
mapObject: mapObject$2,
|
|
14639
|
-
mapObjectFromArray: mapObjectFromArray
|
|
14640
|
-
};
|
|
14641
|
-
|
|
14642
14625
|
/**
|
|
14643
14626
|
* MovableLine
|
|
14644
14627
|
*/
|
|
@@ -14703,7 +14686,7 @@ _.extend(MovableLine$4.prototype, {
|
|
|
14703
14686
|
// - are objects, not primitives (and need a deeper copy)
|
|
14704
14687
|
// - they don't need getters created for them
|
|
14705
14688
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
14706
|
-
|
|
14689
|
+
pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
|
|
14707
14690
|
},
|
|
14708
14691
|
/**
|
|
14709
14692
|
* Resets the object to its state as if it were constructed with
|
|
@@ -15203,7 +15186,7 @@ class MovablePoint$5 {
|
|
|
15203
15186
|
// - are objects, not primitives (and need a deeper copy)
|
|
15204
15187
|
// - they don't need getters created for them
|
|
15205
15188
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
15206
|
-
|
|
15189
|
+
pluck(MovablePointOptions, "standard")
|
|
15207
15190
|
// We only update props here, because we want things on state to
|
|
15208
15191
|
// be persistent, and updated appropriately in modify()
|
|
15209
15192
|
), DEFAULT_PROPS$1);
|
|
@@ -16094,7 +16077,7 @@ _.extend(MovablePolygon.prototype, {
|
|
|
16094
16077
|
// - are objects, not primitives (and need a deeper copy)
|
|
16095
16078
|
// - they don't need getters created for them
|
|
16096
16079
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
16097
|
-
|
|
16080
|
+
pluck(MovablePolygonOptions, "standard")
|
|
16098
16081
|
|
|
16099
16082
|
// We only update props here, because we want things on state to
|
|
16100
16083
|
// be persistent, and updated appropriately in modify()
|
|
@@ -19284,12 +19267,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19284
19267
|
isLintable: isLintable
|
|
19285
19268
|
});
|
|
19286
19269
|
|
|
19287
|
-
const {
|
|
19288
|
-
mapObject: mapObject$1
|
|
19289
|
-
} = objective_;
|
|
19290
19270
|
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
19291
19271
|
// @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; }'.
|
|
19292
|
-
return mapObject
|
|
19272
|
+
return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
19293
19273
|
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19294
19274
|
const newValues = {};
|
|
19295
19275
|
if (!widgetInfo.type) {
|
|
@@ -19317,15 +19297,8 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19317
19297
|
// Static widgets shouldn't count as empty
|
|
19318
19298
|
return false;
|
|
19319
19299
|
}
|
|
19320
|
-
let score = null;
|
|
19321
|
-
const userInput = userInputMap[id];
|
|
19322
19300
|
const scorer = getWidgetScorer(widget.type);
|
|
19323
|
-
|
|
19324
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
|
|
19325
|
-
score = Util.flattenScores(scores);
|
|
19326
|
-
} else if (scorer) {
|
|
19327
|
-
score = scorer(userInput, widget.options, strings, locale);
|
|
19328
|
-
}
|
|
19301
|
+
const score = scorer == null ? void 0 : scorer(userInputMap[id], widget.options, strings, locale);
|
|
19329
19302
|
if (score) {
|
|
19330
19303
|
return Util.scoreIsEmpty(score);
|
|
19331
19304
|
}
|
|
@@ -19365,11 +19338,9 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19365
19338
|
}
|
|
19366
19339
|
const userInput = userInputMap[id];
|
|
19367
19340
|
const scorer = getWidgetScorer(widget.type);
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
widgetScores[id] =
|
|
19371
|
-
} else if (scorer) {
|
|
19372
|
-
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
19341
|
+
const score = scorer == null ? void 0 : scorer(userInput, widget.options, strings, locale);
|
|
19342
|
+
if (score != null) {
|
|
19343
|
+
widgetScores[id] = score;
|
|
19373
19344
|
}
|
|
19374
19345
|
});
|
|
19375
19346
|
return widgetScores;
|
|
@@ -19675,9 +19646,6 @@ WidgetContainer.defaultProps = {
|
|
|
19675
19646
|
linterContext: linterContextDefault
|
|
19676
19647
|
};
|
|
19677
19648
|
|
|
19678
|
-
const {
|
|
19679
|
-
mapObject
|
|
19680
|
-
} = objective_;
|
|
19681
19649
|
const rContainsNonWhitespace = /\S/;
|
|
19682
19650
|
const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
|
|
19683
19651
|
const noopOnRender = () => {};
|
|
@@ -20603,7 +20571,7 @@ class Renderer extends React.Component {
|
|
|
20603
20571
|
}, 0);
|
|
20604
20572
|
});
|
|
20605
20573
|
};
|
|
20606
|
-
this.setInputValue = (path, newValue,
|
|
20574
|
+
this.setInputValue = (path, newValue, cb) => {
|
|
20607
20575
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
20608
20576
|
const widgetId = _.first(path);
|
|
20609
20577
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
@@ -20611,7 +20579,7 @@ class Renderer extends React.Component {
|
|
|
20611
20579
|
const widget = this.getWidgetInstance(widgetId);
|
|
20612
20580
|
|
|
20613
20581
|
// Widget handles parsing of the interWidgetPath.
|
|
20614
|
-
widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue,
|
|
20582
|
+
widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, cb);
|
|
20615
20583
|
};
|
|
20616
20584
|
/**
|
|
20617
20585
|
* Returns an array of all widget IDs in the order they occur in
|
|
@@ -20832,6 +20800,12 @@ class Renderer extends React.Component {
|
|
|
20832
20800
|
isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
|
|
20833
20801
|
});
|
|
20834
20802
|
}
|
|
20803
|
+
/**
|
|
20804
|
+
* Returns an array of widget ids that are empty (meaning widgets where the
|
|
20805
|
+
* learner has not interacted with the widget yet or has not filled in all
|
|
20806
|
+
* fields). For example, the `interactive-graph` widget is considered
|
|
20807
|
+
* empty if the graph is in the starting state.
|
|
20808
|
+
*/
|
|
20835
20809
|
emptyWidgets() {
|
|
20836
20810
|
return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20837
20811
|
}
|
|
@@ -21096,6 +21070,7 @@ class TextInput extends React.Component {
|
|
|
21096
21070
|
value: formattedValue,
|
|
21097
21071
|
type: "text",
|
|
21098
21072
|
"aria-label": labelText,
|
|
21073
|
+
"aria-describedby": this.props["aria-describedby"],
|
|
21099
21074
|
onChange: value => this.props.onChange(value),
|
|
21100
21075
|
placeholder: placeholder,
|
|
21101
21076
|
onFocus: onFocus,
|
|
@@ -21362,6 +21337,7 @@ class Tooltip extends React.Component {
|
|
|
21362
21337
|
}, /*#__PURE__*/React.createElement("div", {
|
|
21363
21338
|
ref: this.tooltipContainerRef,
|
|
21364
21339
|
className: "tooltipContainer",
|
|
21340
|
+
role: "tooltip",
|
|
21365
21341
|
style: {
|
|
21366
21342
|
position: "absolute",
|
|
21367
21343
|
// height must start out undefined, not null, so that
|
|
@@ -21442,9 +21418,15 @@ class InputWithExamples extends React.Component {
|
|
|
21442
21418
|
};
|
|
21443
21419
|
this._renderInput = () => {
|
|
21444
21420
|
const id = this._getUniqueId();
|
|
21421
|
+
const ariaId = `aria-for-${id}`;
|
|
21422
|
+
// Generate text from a known set of format options that will read well in a screen reader
|
|
21423
|
+
const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
|
|
21424
|
+
${this.props.examples.slice(1).join(", or\n")}`
|
|
21425
|
+
// @ts-expect-error TS2550: Property replaceAll does not exist on type string.
|
|
21426
|
+
.replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
|
|
21445
21427
|
const inputProps = {
|
|
21446
21428
|
id: id,
|
|
21447
|
-
"aria-describedby":
|
|
21429
|
+
"aria-describedby": ariaId,
|
|
21448
21430
|
ref: "input",
|
|
21449
21431
|
className: this._getInputClassName(),
|
|
21450
21432
|
labelText: this.props.labelText,
|
|
@@ -21460,7 +21442,12 @@ class InputWithExamples extends React.Component {
|
|
|
21460
21442
|
autoCorrect: "off",
|
|
21461
21443
|
spellCheck: "false"
|
|
21462
21444
|
};
|
|
21463
|
-
return /*#__PURE__*/React.createElement(TextInput, inputProps)
|
|
21445
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, inputProps), /*#__PURE__*/React.createElement("span", {
|
|
21446
|
+
id: ariaId,
|
|
21447
|
+
style: {
|
|
21448
|
+
display: "none"
|
|
21449
|
+
}
|
|
21450
|
+
}, examplesAria));
|
|
21464
21451
|
};
|
|
21465
21452
|
this._handleFocus = () => {
|
|
21466
21453
|
this.props.onFocus();
|
|
@@ -21502,8 +21489,12 @@ class InputWithExamples extends React.Component {
|
|
|
21502
21489
|
}
|
|
21503
21490
|
render() {
|
|
21504
21491
|
const input = this._renderInput();
|
|
21505
|
-
const examplesContent =
|
|
21506
|
-
|
|
21492
|
+
const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
|
|
21493
|
+
: this.props.examples // 2 or more items should display as a list
|
|
21494
|
+
.map((example, index) => {
|
|
21495
|
+
// If the first example is bold, then it is most likely a heading/leading text.
|
|
21496
|
+
// So, it shouldn't be part of the list.
|
|
21497
|
+
return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
|
|
21507
21498
|
}).join("\n");
|
|
21508
21499
|
const showExamples = this.props.shouldShowExamples && this.state.showExamples;
|
|
21509
21500
|
return /*#__PURE__*/React.createElement(Tooltip, {
|
|
@@ -22177,7 +22168,7 @@ function scoreNumericInput(userInput, rubric, strings) {
|
|
|
22177
22168
|
// We may have received TeX; try to parse it before grading.
|
|
22178
22169
|
// If `currentValue` is not TeX, this should be a no-op.
|
|
22179
22170
|
const currentValue = ParseTex(userInput.currentValue);
|
|
22180
|
-
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
|
|
22171
|
+
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
|
|
22181
22172
|
|
|
22182
22173
|
// The coefficient is an attribute of the widget
|
|
22183
22174
|
let localValue = currentValue;
|
|
@@ -22238,7 +22229,8 @@ class NumericInput extends React.Component {
|
|
|
22238
22229
|
this.state = {
|
|
22239
22230
|
// keeps track of the other set of values when switching
|
|
22240
22231
|
// between 0 and finite solutions
|
|
22241
|
-
previousValues: [""]
|
|
22232
|
+
previousValues: [""],
|
|
22233
|
+
isFocused: false
|
|
22242
22234
|
};
|
|
22243
22235
|
// TODO(Nicole, Jeremy): This is maybe never used and should be removed
|
|
22244
22236
|
this.examples = () => {
|
|
@@ -22301,9 +22293,19 @@ class NumericInput extends React.Component {
|
|
|
22301
22293
|
};
|
|
22302
22294
|
this._handleFocus = () => {
|
|
22303
22295
|
this.props.onFocus([]);
|
|
22296
|
+
this.setState(currentState => {
|
|
22297
|
+
return _extends({}, currentState, {
|
|
22298
|
+
isFocused: true
|
|
22299
|
+
});
|
|
22300
|
+
});
|
|
22304
22301
|
};
|
|
22305
22302
|
this._handleBlur = () => {
|
|
22306
22303
|
this.props.onBlur([]);
|
|
22304
|
+
this.setState(currentState => {
|
|
22305
|
+
return _extends({}, currentState, {
|
|
22306
|
+
isFocused: false
|
|
22307
|
+
});
|
|
22308
|
+
});
|
|
22307
22309
|
};
|
|
22308
22310
|
}
|
|
22309
22311
|
static getUserInputFromProps(props) {
|
|
@@ -22346,13 +22348,20 @@ class NumericInput extends React.Component {
|
|
|
22346
22348
|
// component.
|
|
22347
22349
|
const styles = StyleSheet.create({
|
|
22348
22350
|
input: {
|
|
22351
|
+
borderRadius: "3px",
|
|
22352
|
+
borderWidth: this.state.isFocused ? "2px" : "1px",
|
|
22353
|
+
display: "inline-block",
|
|
22354
|
+
fontFamily: `Symbola, "Times New Roman", serif`,
|
|
22355
|
+
fontSize: "18px",
|
|
22356
|
+
height: "32px",
|
|
22357
|
+
lineHeight: "18px",
|
|
22358
|
+
padding: this.state.isFocused ? "4px" : "4px 5px",
|
|
22359
|
+
// account for added focus border thickness
|
|
22349
22360
|
textAlign: this.props.rightAlign ? "right" : "left",
|
|
22350
|
-
width: this.props.size === "small" ? 40 : 80
|
|
22351
|
-
padding: 0,
|
|
22352
|
-
height: "auto"
|
|
22361
|
+
width: this.props.size === "small" ? 40 : 80
|
|
22353
22362
|
}
|
|
22354
22363
|
});
|
|
22355
|
-
return /*#__PURE__*/React.createElement(
|
|
22364
|
+
return /*#__PURE__*/React.createElement(InputWithExamples, {
|
|
22356
22365
|
ref: ref => this.inputRef = ref,
|
|
22357
22366
|
value: this.props.currentValue,
|
|
22358
22367
|
onChange: this.handleChange,
|
|
@@ -22364,7 +22373,7 @@ class NumericInput extends React.Component {
|
|
|
22364
22373
|
id: this.props.widgetId,
|
|
22365
22374
|
disabled: this.props.apiOptions.readOnly,
|
|
22366
22375
|
style: styles.input
|
|
22367
|
-
})
|
|
22376
|
+
});
|
|
22368
22377
|
}
|
|
22369
22378
|
}
|
|
22370
22379
|
|
|
@@ -27260,8 +27269,8 @@ var Definition$1 = {
|
|
|
27260
27269
|
scorer: () => scoreNoop()
|
|
27261
27270
|
};
|
|
27262
27271
|
|
|
27263
|
-
//
|
|
27264
|
-
//
|
|
27272
|
+
// There are no required props, but this component can receive props from any
|
|
27273
|
+
// deprecated widget
|
|
27265
27274
|
class DeprecatedStandin extends React__default.Component {
|
|
27266
27275
|
constructor(...args) {
|
|
27267
27276
|
super(...args);
|
|
@@ -29371,6 +29380,13 @@ const getPromptJSON$f = rendererJSON => {
|
|
|
29371
29380
|
});
|
|
29372
29381
|
};
|
|
29373
29382
|
|
|
29383
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
29384
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
29385
|
+
function scoreGroup(userInput, options, strings, locale) {
|
|
29386
|
+
const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
|
|
29387
|
+
return Util.flattenScores(scores);
|
|
29388
|
+
}
|
|
29389
|
+
|
|
29374
29390
|
// exports has no 'transform'
|
|
29375
29391
|
|
|
29376
29392
|
class Group extends React.Component {
|
|
@@ -29525,6 +29541,9 @@ var Group$1 = {
|
|
|
29525
29541
|
displayName: "Group (SAT only)",
|
|
29526
29542
|
widget: Group,
|
|
29527
29543
|
traverseChildWidgets: traverseChildWidgets,
|
|
29544
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29545
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
|
|
29546
|
+
scorer: scoreGroup,
|
|
29528
29547
|
hidden: true,
|
|
29529
29548
|
isLintable: true
|
|
29530
29549
|
};
|
|
@@ -32234,6 +32253,61 @@ const LegacyGrid = ({
|
|
|
32234
32253
|
|
|
32235
32254
|
// Range is replaced within this file with Interval, but it is used elsewhere
|
|
32236
32255
|
// and exported from the package, so we need to keep it around.
|
|
32256
|
+
/**
|
|
32257
|
+
* Our core set of Perseus widgets.
|
|
32258
|
+
*
|
|
32259
|
+
* This interface is the basis for "registering" all Perseus widget types.
|
|
32260
|
+
* There should be one key/value pair for each supported widget. If you create
|
|
32261
|
+
* a new widget, an entry should be added to this interface. Note that this
|
|
32262
|
+
* only registers the widget options type, you'll also need to register the
|
|
32263
|
+
* widget so that it's available at runtime (@see
|
|
32264
|
+
* {@link file://./widgets.ts#registerWidget}).
|
|
32265
|
+
*
|
|
32266
|
+
* Importantly, the key should be the name that is used in widget IDs. For most
|
|
32267
|
+
* widgets that is the same as the widget option's `type` field. In cases where
|
|
32268
|
+
* a widget has been deprecated and replaced with the deprecated-standin
|
|
32269
|
+
* widget, it should be the original widget type!
|
|
32270
|
+
*
|
|
32271
|
+
* If you define the widget outside of this package, you can still add the new
|
|
32272
|
+
* widget to this interface by writing the following in that package that
|
|
32273
|
+
* contains the widget. TypeScript will merge that definition of the
|
|
32274
|
+
* `PerseusWidgets` with the one defined below.
|
|
32275
|
+
*
|
|
32276
|
+
* ```typescript
|
|
32277
|
+
* declare module "@khanacademy/perseus" {
|
|
32278
|
+
* interface PerseusWidgetTypes {
|
|
32279
|
+
* // A new widget
|
|
32280
|
+
* "new-awesomeness": MyAwesomeNewWidget;
|
|
32281
|
+
*
|
|
32282
|
+
* // A deprecated widget
|
|
32283
|
+
* "super-old-widget": AutoCorrectWidget;
|
|
32284
|
+
* }
|
|
32285
|
+
* }
|
|
32286
|
+
*
|
|
32287
|
+
* // The new widget's options definition
|
|
32288
|
+
* type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
|
|
32289
|
+
*
|
|
32290
|
+
* // The deprecated widget's options definition
|
|
32291
|
+
* type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
|
|
32292
|
+
* ```
|
|
32293
|
+
*
|
|
32294
|
+
* This interface can be extended through the magic of TypeScript "Declaration
|
|
32295
|
+
* merging". Specifically, we augment this module and extend this interface.
|
|
32296
|
+
*
|
|
32297
|
+
* @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
|
|
32298
|
+
*/
|
|
32299
|
+
/**
|
|
32300
|
+
* A map of widget IDs to widget options. This is most often used as the type
|
|
32301
|
+
* for a set of widgets defined in a `PerseusItem` but can also be useful to
|
|
32302
|
+
* represent a function parameter where only `widgets` from a `PerseusItem` are
|
|
32303
|
+
* needed. Today Widget IDs are made up of the widget type and an incrementing
|
|
32304
|
+
* integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
|
|
32305
|
+
* reading/parsing the widget id to derive any information from it, except in
|
|
32306
|
+
* the case of this map.
|
|
32307
|
+
*
|
|
32308
|
+
* @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
|
|
32309
|
+
* by augmenting the PerseusWidgetTypes with new widget types!
|
|
32310
|
+
*/
|
|
32237
32311
|
/**
|
|
32238
32312
|
* A "PerseusItem" is a classic Perseus item. It is rendered by the
|
|
32239
32313
|
* `ServerItemRenderer` and the layout is pre-set.
|
|
@@ -32249,6 +32323,9 @@ const LegacyGrid = ({
|
|
|
32249
32323
|
* A "MultiItem" is an advanced Perseus item. It is rendered by the
|
|
32250
32324
|
* `MultiRenderer` and you can control the layout of individual parts of the
|
|
32251
32325
|
* item.
|
|
32326
|
+
*
|
|
32327
|
+
* @deprecated MultiItem support is slated for removal in a future Perseus
|
|
32328
|
+
* release.
|
|
32252
32329
|
*/
|
|
32253
32330
|
const ItemExtras = [
|
|
32254
32331
|
// The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
|
|
@@ -32270,6 +32347,11 @@ const ItemExtras = [
|
|
|
32270
32347
|
// The user might benefit from using a statistics Z Table like https://www.ztable.net/
|
|
32271
32348
|
"zTable"];
|
|
32272
32349
|
|
|
32350
|
+
/**
|
|
32351
|
+
* The type representing the common structure of all widget's options. The
|
|
32352
|
+
* `Options` generic type represents the widget-specific option data.
|
|
32353
|
+
*/
|
|
32354
|
+
|
|
32273
32355
|
// prettier-ignore
|
|
32274
32356
|
|
|
32275
32357
|
// prettier-ignore
|
|
@@ -32338,7 +32420,9 @@ const ItemExtras = [
|
|
|
32338
32420
|
|
|
32339
32421
|
//prettier-ignore
|
|
32340
32422
|
|
|
32341
|
-
|
|
32423
|
+
/**
|
|
32424
|
+
* A background image applied to various widgets.
|
|
32425
|
+
*/
|
|
32342
32426
|
|
|
32343
32427
|
const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
|
|
32344
32428
|
|
|
@@ -36906,8 +36990,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
|
|
|
36906
36990
|
});
|
|
36907
36991
|
};
|
|
36908
36992
|
|
|
36909
|
-
// There's no transform function in exports
|
|
36910
|
-
|
|
36911
36993
|
// TODO: there's another, very similar getSinusoidCoefficients function
|
|
36912
36994
|
// they should probably be merged
|
|
36913
36995
|
function getSinusoidCoefficients(coords) {
|
|
@@ -42043,23 +42125,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
|
|
|
42043
42125
|
};
|
|
42044
42126
|
};
|
|
42045
42127
|
|
|
42046
|
-
|
|
42047
|
-
|
|
42048
|
-
|
|
42049
|
-
|
|
42050
|
-
|
|
42051
|
-
|
|
42052
|
-
|
|
42053
|
-
const
|
|
42128
|
+
/**
|
|
42129
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
42130
|
+
* state.
|
|
42131
|
+
* @param userInput
|
|
42132
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
42133
|
+
*/
|
|
42134
|
+
function validateNumberLine(userInput) {
|
|
42135
|
+
const divisionRange = userInput.divisionRange;
|
|
42136
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
42054
42137
|
|
|
42055
42138
|
// TODO: I don't think isTickCrtl is a thing anymore
|
|
42056
|
-
if (
|
|
42139
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
42057
42140
|
return {
|
|
42058
42141
|
type: "invalid",
|
|
42059
42142
|
message: "Number of divisions is outside the allowed range."
|
|
42060
42143
|
};
|
|
42061
42144
|
}
|
|
42062
|
-
|
|
42145
|
+
return null;
|
|
42146
|
+
}
|
|
42147
|
+
|
|
42148
|
+
function scoreNumberLine(userInput, scoringData) {
|
|
42149
|
+
const validationError = validateNumberLine(userInput);
|
|
42150
|
+
if (validationError) {
|
|
42151
|
+
return validationError;
|
|
42152
|
+
}
|
|
42153
|
+
const range = scoringData.range;
|
|
42154
|
+
const start = scoringData.initialX != null ? scoringData.initialX : range[0];
|
|
42155
|
+
const startRel = scoringData.isInequality ? "ge" : "eq";
|
|
42156
|
+
const correctRel = scoringData.correctRel || "eq";
|
|
42157
|
+
const correctPos = number$1.equal(userInput.numLinePosition, scoringData.correctX || 0);
|
|
42158
|
+
if (correctPos && correctRel === userInput.rel) {
|
|
42063
42159
|
return {
|
|
42064
42160
|
type: "points",
|
|
42065
42161
|
earned: 1,
|
|
@@ -42067,7 +42163,7 @@ function scoreNumberLine(state, rubric) {
|
|
|
42067
42163
|
message: null
|
|
42068
42164
|
};
|
|
42069
42165
|
}
|
|
42070
|
-
if (
|
|
42166
|
+
if (userInput.numLinePosition === start && userInput.rel === startRel) {
|
|
42071
42167
|
// We're where we started.
|
|
42072
42168
|
return {
|
|
42073
42169
|
type: "invalid",
|
|
@@ -45013,7 +45109,7 @@ const init = function init() {
|
|
|
45013
45109
|
|
|
45014
45110
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45015
45111
|
const libName = "@khanacademy/perseus";
|
|
45016
|
-
const libVersion = "45.
|
|
45112
|
+
const libVersion = "45.1.0";
|
|
45017
45113
|
addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45018
45114
|
|
|
45019
45115
|
const apiVersion = {
|
|
@@ -45621,14 +45717,10 @@ class ServerItemRenderer extends React.Component {
|
|
|
45621
45717
|
});
|
|
45622
45718
|
}
|
|
45623
45719
|
componentDidUpdate(prevProps, prevState) {
|
|
45624
|
-
|
|
45720
|
+
const answerableCallback = this.props.apiOptions.answerableCallback;
|
|
45721
|
+
if (answerableCallback != null) {
|
|
45625
45722
|
const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
|
|
45626
|
-
|
|
45627
|
-
answerableCallback
|
|
45628
|
-
} = this.props.apiOptions;
|
|
45629
|
-
if (answerableCallback) {
|
|
45630
|
-
answerableCallback(isAnswerable);
|
|
45631
|
-
}
|
|
45723
|
+
answerableCallback(isAnswerable);
|
|
45632
45724
|
}
|
|
45633
45725
|
if (!this._fullyRendered) {
|
|
45634
45726
|
const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
|
|
@@ -45971,7 +46063,7 @@ const traverseRenderer$1 = function traverseRenderer(rendererOptions, contentCal
|
|
|
45971
46063
|
newContent = modifiedContent;
|
|
45972
46064
|
}
|
|
45973
46065
|
}
|
|
45974
|
-
const newWidgets =
|
|
46066
|
+
const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
|
|
45975
46067
|
// Widgets without info or a type are empty widgets, and
|
|
45976
46068
|
// should always be renderable. It's also annoying to write
|
|
45977
46069
|
// checks for this everywhere, so we just filter them out once and
|
|
@@ -48570,7 +48662,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
|
|
|
48570
48662
|
const parseDefinitionWidget = parseWidget(constant("definition"), object({
|
|
48571
48663
|
togglePrompt: string,
|
|
48572
48664
|
definition: string,
|
|
48573
|
-
static: boolean
|
|
48665
|
+
static: defaulted(boolean, () => false)
|
|
48574
48666
|
}));
|
|
48575
48667
|
|
|
48576
48668
|
const parseDropdownWidget = parseWidget(constant("dropdown"), object({
|
|
@@ -48772,15 +48864,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
|
|
|
48772
48864
|
static: defaulted(boolean, () => false)
|
|
48773
48865
|
}));
|
|
48774
48866
|
|
|
48867
|
+
const stringToNumber = (rawValue, ctx) => {
|
|
48868
|
+
if (typeof rawValue === "number") {
|
|
48869
|
+
return ctx.success(rawValue);
|
|
48870
|
+
}
|
|
48871
|
+
const parsedNumber = +rawValue;
|
|
48872
|
+
if (rawValue === "" || isNaN(parsedNumber)) {
|
|
48873
|
+
return ctx.failure("a number or numeric string", rawValue);
|
|
48874
|
+
}
|
|
48875
|
+
return ctx.success(parsedNumber);
|
|
48876
|
+
};
|
|
48877
|
+
|
|
48878
|
+
const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
|
|
48775
48879
|
const parsePerseusImageBackground = object({
|
|
48776
48880
|
url: optional(nullable(string)),
|
|
48777
|
-
width: optional(
|
|
48778
|
-
height: optional(
|
|
48779
|
-
top: optional(
|
|
48780
|
-
left: optional(
|
|
48781
|
-
bottom: optional(
|
|
48782
|
-
|
|
48783
|
-
scale: optional(union(number).or(string).parser)
|
|
48881
|
+
width: optional(numericToNumber),
|
|
48882
|
+
height: optional(numericToNumber),
|
|
48883
|
+
top: optional(numericToNumber),
|
|
48884
|
+
left: optional(numericToNumber),
|
|
48885
|
+
bottom: optional(numericToNumber),
|
|
48886
|
+
scale: optional(numericToNumber)
|
|
48784
48887
|
});
|
|
48785
48888
|
|
|
48786
48889
|
const pairOfNumbers$2 = pair(number, number);
|
|
@@ -48799,6 +48902,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
|
|
|
48799
48902
|
box: optional(pairOfNumbers$2)
|
|
48800
48903
|
}));
|
|
48801
48904
|
|
|
48905
|
+
const booleanToString = (rawValue, ctx) => {
|
|
48906
|
+
if (typeof rawValue === "boolean") {
|
|
48907
|
+
return ctx.success(String(rawValue));
|
|
48908
|
+
}
|
|
48909
|
+
return ctx.failure("boolean", rawValue);
|
|
48910
|
+
};
|
|
48802
48911
|
const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
48803
48912
|
answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
|
|
48804
48913
|
inexact: optional(boolean),
|
|
@@ -48806,7 +48915,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
|
48806
48915
|
rightAlign: optional(boolean),
|
|
48807
48916
|
simplify: enumeration("required", "optional", "enforced"),
|
|
48808
48917
|
size: enumeration("normal", "small"),
|
|
48809
|
-
|
|
48918
|
+
// TODO(benchristel): there are some content items where value is a
|
|
48919
|
+
// boolean, even though that makes no sense. We should figure out if
|
|
48920
|
+
// those content items are actually published anywhere, and consider
|
|
48921
|
+
// updating them.
|
|
48922
|
+
value: union(number).or(string).or(booleanToString).parser,
|
|
48810
48923
|
customKeypad: optional(boolean)
|
|
48811
48924
|
}));
|
|
48812
48925
|
|
|
@@ -49104,18 +49217,27 @@ const parseLockedFunctionType = object({
|
|
|
49104
49217
|
const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
|
|
49105
49218
|
const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
|
|
49106
49219
|
step: pairOfNumbers,
|
|
49107
|
-
|
|
49108
|
-
|
|
49220
|
+
// TODO(benchristel): rather than making gridStep and snapStep
|
|
49221
|
+
// optional, we should duplicate the defaulting logic from the
|
|
49222
|
+
// InteractiveGraph component. See parse-perseus-json/README.md for
|
|
49223
|
+
// why.
|
|
49224
|
+
gridStep: optional(pairOfNumbers),
|
|
49225
|
+
snapStep: optional(pairOfNumbers),
|
|
49109
49226
|
backgroundImage: optional(parsePerseusImageBackground),
|
|
49110
49227
|
markings: enumeration("graph", "grid", "none"),
|
|
49111
|
-
labels: array(string),
|
|
49228
|
+
labels: optional(array(string)),
|
|
49112
49229
|
showProtractor: boolean,
|
|
49113
49230
|
showRuler: optional(boolean),
|
|
49114
49231
|
showTooltips: optional(boolean),
|
|
49115
49232
|
rulerLabel: optional(string),
|
|
49116
49233
|
rulerTicks: optional(number),
|
|
49117
49234
|
range: pair(pairOfNumbers, pairOfNumbers),
|
|
49118
|
-
|
|
49235
|
+
// NOTE(benchristel): I copied the default graph from
|
|
49236
|
+
// interactive-graph.tsx. See the parse-perseus-json/README.md for
|
|
49237
|
+
// an explanation of why we want to duplicate the default here.
|
|
49238
|
+
graph: defaulted(parsePerseusGraphType, () => ({
|
|
49239
|
+
type: "linear"
|
|
49240
|
+
})),
|
|
49119
49241
|
correct: parsePerseusGraphType,
|
|
49120
49242
|
// TODO(benchristel): default lockedFigures to empty array
|
|
49121
49243
|
lockedFigures: optional(array(parseLockedFigure)),
|
|
@@ -49183,7 +49305,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
|
|
|
49183
49305
|
isTickCtrl: optional(nullable(boolean)),
|
|
49184
49306
|
divisionRange: array(number),
|
|
49185
49307
|
numDivisions: optional(nullable(number)),
|
|
49186
|
-
|
|
49308
|
+
// NOTE(benchristel): I copied the default snapDivisions from
|
|
49309
|
+
// number-line.tsx. See the parse-perseus-json/README.md for
|
|
49310
|
+
// an explanation of why we want to duplicate the default here.
|
|
49311
|
+
snapDivisions: defaulted(number, () => 2),
|
|
49187
49312
|
tickStep: optional(nullable(number)),
|
|
49188
49313
|
correctRel: optional(nullable(string)),
|
|
49189
49314
|
correctX: number,
|
|
@@ -49196,18 +49321,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
|
|
|
49196
49321
|
const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
49197
49322
|
answers: array(object({
|
|
49198
49323
|
message: string,
|
|
49199
|
-
value: number,
|
|
49324
|
+
value: optional(number),
|
|
49200
49325
|
status: string,
|
|
49201
49326
|
answerForms: optional(array(parseMathFormat)),
|
|
49202
49327
|
strict: boolean,
|
|
49203
49328
|
maxError: optional(nullable(number)),
|
|
49204
49329
|
simplify: optional(nullable(string))
|
|
49205
49330
|
})),
|
|
49206
|
-
labelText: string,
|
|
49331
|
+
labelText: optional(string),
|
|
49207
49332
|
size: string,
|
|
49208
49333
|
coefficient: boolean,
|
|
49209
49334
|
rightAlign: optional(boolean),
|
|
49210
|
-
static: boolean,
|
|
49335
|
+
static: defaulted(boolean, () => false),
|
|
49211
49336
|
answerForms: optional(array(object({
|
|
49212
49337
|
name: parseMathFormat,
|
|
49213
49338
|
simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
|
|
@@ -49220,12 +49345,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
|
49220
49345
|
function parseRenderer(rawValue, ctx) {
|
|
49221
49346
|
return parsePerseusRenderer(rawValue, ctx);
|
|
49222
49347
|
}
|
|
49348
|
+
const largeToAuto = (height, ctx) => {
|
|
49349
|
+
if (height === "large") {
|
|
49350
|
+
return ctx.success("auto");
|
|
49351
|
+
}
|
|
49352
|
+
return ctx.success(height);
|
|
49353
|
+
};
|
|
49223
49354
|
const parseOrdererWidget = parseWidget(constant("orderer"), object({
|
|
49224
|
-
options: array(parseRenderer),
|
|
49355
|
+
options: defaulted(array(parseRenderer), () => []),
|
|
49225
49356
|
correctOptions: array(parseRenderer),
|
|
49226
49357
|
otherOptions: array(parseRenderer),
|
|
49227
|
-
height: enumeration("normal", "auto"),
|
|
49228
|
-
layout: enumeration("horizontal", "vertical")
|
|
49358
|
+
height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
|
|
49359
|
+
layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
|
|
49229
49360
|
}));
|
|
49230
49361
|
|
|
49231
49362
|
const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
|
|
@@ -49239,7 +49370,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
|
|
|
49239
49370
|
passageText: string,
|
|
49240
49371
|
passageTitle: string,
|
|
49241
49372
|
showLineNumbers: boolean,
|
|
49242
|
-
static: boolean
|
|
49373
|
+
static: defaulted(boolean, () => false)
|
|
49243
49374
|
}));
|
|
49244
49375
|
|
|
49245
49376
|
const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
|
|
@@ -49260,7 +49391,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
|
|
|
49260
49391
|
picUrl: optional(nullable(string)),
|
|
49261
49392
|
picSize: optional(nullable(number)),
|
|
49262
49393
|
picBoxHeight: optional(nullable(number)),
|
|
49263
|
-
|
|
49394
|
+
// NOTE(benchristel): I copied the default plotDimensions from
|
|
49395
|
+
// plotter.tsx. See the parse-perseus-json/README.md for an explanation
|
|
49396
|
+
// of why we want to duplicate the defaults here.
|
|
49397
|
+
plotDimensions: defaulted(array(number), () => [380, 300])
|
|
49264
49398
|
}));
|
|
49265
49399
|
|
|
49266
49400
|
const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
|
|
@@ -49412,6 +49546,10 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
|
|
|
49412
49546
|
return parseAndAssign(`table ${id}`, parseTableWidget);
|
|
49413
49547
|
case "video":
|
|
49414
49548
|
return parseAndAssign(`video ${id}`, parseVideoWidget);
|
|
49549
|
+
case "sequence":
|
|
49550
|
+
// sequence is a deprecated widget type, and the corresponding
|
|
49551
|
+
// widget component no longer exists.
|
|
49552
|
+
return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
|
|
49415
49553
|
default:
|
|
49416
49554
|
if (getWidget(type)) {
|
|
49417
49555
|
// @ts-expect-error - 'type' is not a valid widget type
|
|
@@ -49420,6 +49558,11 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
|
|
|
49420
49558
|
return ctx.failure("a valid widget type", type);
|
|
49421
49559
|
}
|
|
49422
49560
|
};
|
|
49561
|
+
const parseDeprecatedWidget = parseWidget(
|
|
49562
|
+
// Ignore the incoming widget type and hardcode "deprecated-standin"
|
|
49563
|
+
(_, ctx) => ctx.success("deprecated-standin"),
|
|
49564
|
+
// Allow any widget options
|
|
49565
|
+
object({}));
|
|
49423
49566
|
const parseStringToPositiveInt = (rawValue, ctx) => {
|
|
49424
49567
|
if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
|
|
49425
49568
|
return ctx.failure("numeric string", rawValue);
|