@khanacademy/perseus 43.1.0 → 45.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/all-widgets.d.ts +2 -23
- package/dist/basic-widgets.d.ts +2 -23
- package/dist/components/graph.d.ts +1 -0
- package/dist/components/icon.d.ts +0 -1
- package/dist/components/multi-button-group.d.ts +1 -0
- package/dist/components/tooltip.d.ts +1 -0
- package/dist/es/index.js +276 -212
- package/dist/es/index.js.map +1 -1
- package/dist/es/strings.js +4 -2
- package/dist/es/strings.js.map +1 -1
- package/dist/extra-widgets.d.ts +2 -23
- package/dist/index.d.ts +4 -2
- package/dist/index.js +273 -205
- package/dist/index.js.map +1 -1
- package/dist/interactive2/interactive-util.d.ts +1 -1
- package/dist/interactive2/movable-point-options.d.ts +2 -2
- package/dist/interactive2/movable-point.d.ts +7 -7
- package/dist/interactive2/objective_.d.ts +1 -1
- package/dist/jipt-paragraphs.d.ts +2 -2
- package/dist/perseus-api.d.ts +2 -77
- package/dist/perseus-types.d.ts +3 -10
- package/dist/renderer-util.d.ts +4 -2
- package/dist/renderer.d.ts +6 -10
- package/dist/server-item-renderer.d.ts +58 -50
- package/dist/strings.d.ts +1 -0
- package/dist/strings.js +4 -2
- package/dist/strings.js.map +1 -1
- package/dist/styles/global-styles.d.ts +0 -1
- package/dist/styles/shared.d.ts +0 -1
- package/dist/traversal.d.ts +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/util/a11y.d.ts +0 -1
- package/dist/util/debounce.d.ts +1 -1
- package/dist/util/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +2 -2
- package/dist/widgets/definition/definition.d.ts +1 -1
- package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
- package/dist/widgets/dropdown/dropdown.d.ts +9 -1
- package/dist/widgets/explanation/explanation.d.ts +1 -1
- package/dist/widgets/expression/expression.d.ts +92 -217
- package/dist/widgets/group/group.d.ts +1 -1
- package/dist/widgets/image/image.d.ts +1 -1
- package/dist/widgets/interaction/interaction.d.ts +1 -1
- package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +1 -1
- package/dist/widgets/label-image/answer-pill.d.ts +0 -1
- package/dist/widgets/label-image/label-image.d.ts +5 -65
- package/dist/widgets/label-image/marker.d.ts +0 -1
- package/dist/widgets/measurer/measurer.d.ts +1 -1
- package/dist/widgets/molecule/molecule.d.ts +1 -1
- package/dist/widgets/passage/passage-markdown.d.ts +2 -2
- package/dist/widgets/passage/passage.d.ts +1 -1
- package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
- package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +1 -1
- package/dist/widgets/radio/choice-icon/shared-styles.d.ts +0 -1
- package/dist/widgets/radio/choice-none-above.d.ts +1 -1
- package/dist/widgets/radio/radio.d.ts +2 -2
- package/dist/widgets/video/video.d.ts +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -384,7 +384,8 @@ const mockStrings = {
|
|
|
384
384
|
} = _ref28;
|
|
385
385
|
return `Interactive elements: ${elements}`;
|
|
386
386
|
},
|
|
387
|
-
srNoInteractiveElements: "No interactive elements"
|
|
387
|
+
srNoInteractiveElements: "No interactive elements",
|
|
388
|
+
selectAnAnswer: "Select an answer"
|
|
388
389
|
};
|
|
389
390
|
|
|
390
391
|
/**
|
|
@@ -14756,9 +14757,9 @@ const pluck = function (table, subKey) {
|
|
|
14756
14757
|
* });
|
|
14757
14758
|
* {a: 2, b: 3}
|
|
14758
14759
|
*/
|
|
14759
|
-
const mapObject$
|
|
14760
|
+
const mapObject$2 = function (obj, lambda) {
|
|
14760
14761
|
const result = {};
|
|
14761
|
-
|
|
14762
|
+
Object.keys(obj).forEach(key => {
|
|
14762
14763
|
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
14763
14764
|
result[key] = lambda(obj[key], key);
|
|
14764
14765
|
});
|
|
@@ -14775,14 +14776,14 @@ const mapObject$1 = function (obj, lambda) {
|
|
|
14775
14776
|
*/
|
|
14776
14777
|
const mapObjectFromArray = function (arr, lambda) {
|
|
14777
14778
|
const result = {};
|
|
14778
|
-
|
|
14779
|
+
arr.forEach(elem => {
|
|
14779
14780
|
result[elem] = lambda(elem);
|
|
14780
14781
|
});
|
|
14781
14782
|
return result;
|
|
14782
14783
|
};
|
|
14783
14784
|
var objective_ = {
|
|
14784
14785
|
pluck: pluck,
|
|
14785
|
-
mapObject: mapObject$
|
|
14786
|
+
mapObject: mapObject$2,
|
|
14786
14787
|
mapObjectFromArray: mapObjectFromArray
|
|
14787
14788
|
};
|
|
14788
14789
|
|
|
@@ -16829,7 +16830,7 @@ class Graphie extends React__namespace.Component {
|
|
|
16829
16830
|
};
|
|
16830
16831
|
_setupGraphie = () => {
|
|
16831
16832
|
this._removeMovables();
|
|
16832
|
-
const graphieDiv =
|
|
16833
|
+
const graphieDiv = this.graphieDivRef.current;
|
|
16833
16834
|
if (graphieDiv == null || graphieDiv instanceof Text) {
|
|
16834
16835
|
throw new Error("No graphie container div found");
|
|
16835
16836
|
}
|
|
@@ -18918,6 +18919,138 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18918
18919
|
}
|
|
18919
18920
|
}
|
|
18920
18921
|
|
|
18922
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
18923
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
18924
|
+
|
|
18925
|
+
/**
|
|
18926
|
+
* Regex for widget placeholders in a string.
|
|
18927
|
+
*
|
|
18928
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
18929
|
+
* Second capture group is the widget type (ex. "radio)
|
|
18930
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
18931
|
+
*/
|
|
18932
|
+
function getWidgetRegex() {
|
|
18933
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
18934
|
+
}
|
|
18935
|
+
|
|
18936
|
+
/**
|
|
18937
|
+
* Add a widget placeholder using the widget ID.
|
|
18938
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
18939
|
+
*
|
|
18940
|
+
* @param {string} id
|
|
18941
|
+
* @returns {string}
|
|
18942
|
+
*/
|
|
18943
|
+
function addWidget(id) {
|
|
18944
|
+
return `[[☃ ${id}]]`;
|
|
18945
|
+
}
|
|
18946
|
+
|
|
18947
|
+
/**
|
|
18948
|
+
* Get a widget type by a widget's ID
|
|
18949
|
+
* (please don't derive type from ID)
|
|
18950
|
+
*
|
|
18951
|
+
* @param {string} widgetId the ID of the widget
|
|
18952
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18953
|
+
* @returns {string} the widget type (ie "radio")
|
|
18954
|
+
*/
|
|
18955
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
18956
|
+
const widget = widgetMap[WidgetId];
|
|
18957
|
+
return widget?.type ?? null;
|
|
18958
|
+
}
|
|
18959
|
+
|
|
18960
|
+
/**
|
|
18961
|
+
* Does the content have a specific type of widget?
|
|
18962
|
+
*
|
|
18963
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
18964
|
+
* @param {string} content the string to search through
|
|
18965
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18966
|
+
* @returns {boolean} if the content includes the widget type
|
|
18967
|
+
*/
|
|
18968
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
18969
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
18970
|
+
}
|
|
18971
|
+
|
|
18972
|
+
/**
|
|
18973
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
18974
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
18975
|
+
*
|
|
18976
|
+
* Content should contain Perseus widget placeholders,
|
|
18977
|
+
* which look like: '[[☃ radio 1]]'.
|
|
18978
|
+
*
|
|
18979
|
+
* @param {string} content
|
|
18980
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
18981
|
+
*/
|
|
18982
|
+
function getWidgetIdsFromContent(content) {
|
|
18983
|
+
const widgets = [];
|
|
18984
|
+
const localWidgetRegex = getWidgetRegex();
|
|
18985
|
+
let match = localWidgetRegex.exec(content);
|
|
18986
|
+
while (match !== null) {
|
|
18987
|
+
widgets.push(match[1]);
|
|
18988
|
+
match = localWidgetRegex.exec(content);
|
|
18989
|
+
}
|
|
18990
|
+
return widgets;
|
|
18991
|
+
}
|
|
18992
|
+
|
|
18993
|
+
/**
|
|
18994
|
+
* Get a list of widget IDs from content,
|
|
18995
|
+
* but only for specific widget types
|
|
18996
|
+
*
|
|
18997
|
+
* @param {string} type the type of widget (ie "radio")
|
|
18998
|
+
* @param {string} content the string to parse
|
|
18999
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19000
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
19001
|
+
*/
|
|
19002
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
19003
|
+
const rv = [];
|
|
19004
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
19005
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
19006
|
+
const widget = widgetMap[widgetId];
|
|
19007
|
+
if (widget?.type === type) {
|
|
19008
|
+
rv.push(widgetId);
|
|
19009
|
+
}
|
|
19010
|
+
});
|
|
19011
|
+
return rv;
|
|
19012
|
+
}
|
|
19013
|
+
|
|
19014
|
+
/**
|
|
19015
|
+
* Pull the widget map out of ItemData
|
|
19016
|
+
*
|
|
19017
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
19018
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
19019
|
+
*/
|
|
19020
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
19021
|
+
return itemData.question.widgets;
|
|
19022
|
+
}
|
|
19023
|
+
|
|
19024
|
+
/**
|
|
19025
|
+
* Get the widget information from a WidgetMap
|
|
19026
|
+
*
|
|
19027
|
+
* @param {string} widgetId the ID of the widget
|
|
19028
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
19029
|
+
*/
|
|
19030
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
19031
|
+
return widgetMap[widgetId] ?? null;
|
|
19032
|
+
}
|
|
19033
|
+
|
|
19034
|
+
/**
|
|
19035
|
+
* Get select widgets from a widget map.
|
|
19036
|
+
* Useful for multi-items that needs to split
|
|
19037
|
+
* one PerseusItem into several
|
|
19038
|
+
*
|
|
19039
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
19040
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
19041
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
19042
|
+
*/
|
|
19043
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
19044
|
+
const widgets = {};
|
|
19045
|
+
widgetIds.forEach(widgetId => {
|
|
19046
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
19047
|
+
if (widget) {
|
|
19048
|
+
widgets[widgetId] = widget;
|
|
19049
|
+
}
|
|
19050
|
+
});
|
|
19051
|
+
return widgets;
|
|
19052
|
+
}
|
|
19053
|
+
|
|
18921
19054
|
const DEFAULT_ALIGNMENT$1 = "block";
|
|
18922
19055
|
// NOTE(kevinb): "default" is not one in `validAlignments`.
|
|
18923
19056
|
const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
|
|
@@ -19348,12 +19481,38 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19348
19481
|
isLintable: isLintable
|
|
19349
19482
|
});
|
|
19350
19483
|
|
|
19484
|
+
const {
|
|
19485
|
+
mapObject: mapObject$1
|
|
19486
|
+
} = objective_;
|
|
19487
|
+
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
19488
|
+
// @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$1(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
19490
|
+
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19491
|
+
const newValues = {};
|
|
19492
|
+
if (!widgetInfo.type) {
|
|
19493
|
+
// TODO: why does widget have no type?
|
|
19494
|
+
// We don't want to derive type from widget ID
|
|
19495
|
+
// see: LEMS-1845
|
|
19496
|
+
newValues.type = widgetId.split(" ")[0];
|
|
19497
|
+
}
|
|
19498
|
+
if (!widgetInfo.alignment) {
|
|
19499
|
+
newValues.alignment = "default";
|
|
19500
|
+
}
|
|
19501
|
+
widgetInfo = {
|
|
19502
|
+
...widgetInfo,
|
|
19503
|
+
...newValues
|
|
19504
|
+
};
|
|
19505
|
+
}
|
|
19506
|
+
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
19507
|
+
});
|
|
19508
|
+
}
|
|
19351
19509
|
function emptyWidgetsFunctional(widgets,
|
|
19352
19510
|
// This is a port of old code, I'm not sure why
|
|
19353
19511
|
// we need widgetIds vs the keys of the widgets object
|
|
19354
19512
|
widgetIds, userInputMap, strings, locale) {
|
|
19513
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
19355
19514
|
return widgetIds.filter(id => {
|
|
19356
|
-
const widget =
|
|
19515
|
+
const widget = upgradedWidgets[id];
|
|
19357
19516
|
if (!widget || widget.static) {
|
|
19358
19517
|
// Static widgets shouldn't count as empty
|
|
19359
19518
|
return false;
|
|
@@ -19372,12 +19531,27 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19372
19531
|
}
|
|
19373
19532
|
});
|
|
19374
19533
|
}
|
|
19534
|
+
|
|
19535
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
19536
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
19537
|
+
function scorePerseusItem(perseusRenderData, userInputMap,
|
|
19538
|
+
// TODO(LEMS-2461,LEMS-2391): these probably
|
|
19539
|
+
// need to be removed before we move this to the server
|
|
19540
|
+
strings, locale) {
|
|
19541
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
19542
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
19543
|
+
// so this checks that the widgets are being used before scoring them
|
|
19544
|
+
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
|
|
19545
|
+
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
|
|
19546
|
+
return Util.flattenScores(scores);
|
|
19547
|
+
}
|
|
19375
19548
|
function scoreWidgetsFunctional(widgets,
|
|
19376
19549
|
// This is a port of old code, I'm not sure why
|
|
19377
19550
|
// we need widgetIds vs the keys of the widgets object
|
|
19378
19551
|
widgetIds, userInputMap, strings, locale) {
|
|
19552
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
19379
19553
|
const gradedWidgetIds = widgetIds.filter(id => {
|
|
19380
|
-
const props =
|
|
19554
|
+
const props = upgradedWidgets[id];
|
|
19381
19555
|
const widgetIsGraded = props?.graded == null || props.graded;
|
|
19382
19556
|
const widgetIsStatic = !!props?.static;
|
|
19383
19557
|
// Ungraded widgets or widgets set to static shouldn't be graded.
|
|
@@ -19385,14 +19559,14 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19385
19559
|
});
|
|
19386
19560
|
const widgetScores = {};
|
|
19387
19561
|
gradedWidgetIds.forEach(id => {
|
|
19388
|
-
const widget =
|
|
19562
|
+
const widget = upgradedWidgets[id];
|
|
19389
19563
|
if (!widget) {
|
|
19390
19564
|
return;
|
|
19391
19565
|
}
|
|
19392
19566
|
const userInput = userInputMap[id];
|
|
19393
19567
|
const scorer = getWidgetScorer(widget.type);
|
|
19394
19568
|
if (widget.type === "group") {
|
|
19395
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets,
|
|
19569
|
+
const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
|
|
19396
19570
|
widgetScores[id] = Util.flattenScores(scores);
|
|
19397
19571
|
} else if (scorer) {
|
|
19398
19572
|
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
@@ -19917,32 +20091,12 @@ class Renderer extends React__namespace.Component {
|
|
|
19917
20091
|
};
|
|
19918
20092
|
};
|
|
19919
20093
|
_getInitialWidgetState = props => {
|
|
19920
|
-
const allWidgetInfo =
|
|
20094
|
+
const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
|
|
19921
20095
|
return {
|
|
19922
20096
|
widgetInfo: allWidgetInfo,
|
|
19923
20097
|
widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
|
|
19924
20098
|
};
|
|
19925
20099
|
};
|
|
19926
|
-
|
|
19927
|
-
// @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; }'.
|
|
19928
|
-
_getAllWidgetsInfo = props => {
|
|
19929
|
-
return mapObject(props.widgets, (widgetInfo, widgetId) => {
|
|
19930
|
-
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19931
|
-
const newValues = {};
|
|
19932
|
-
if (!widgetInfo.type) {
|
|
19933
|
-
// TODO: why does widget have no type?
|
|
19934
|
-
// We don't want to derive type from widget ID
|
|
19935
|
-
// see: LEMS-1845
|
|
19936
|
-
newValues.type = widgetId.split(" ")[0];
|
|
19937
|
-
}
|
|
19938
|
-
if (!widgetInfo.alignment) {
|
|
19939
|
-
newValues.alignment = "default";
|
|
19940
|
-
}
|
|
19941
|
-
widgetInfo = ___default["default"].extend({}, widgetInfo, newValues);
|
|
19942
|
-
}
|
|
19943
|
-
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
19944
|
-
});
|
|
19945
|
-
};
|
|
19946
20100
|
_getAllWidgetsStartProps = (allWidgetInfo, props) => {
|
|
19947
20101
|
const {
|
|
19948
20102
|
apiOptions,
|
|
@@ -20441,7 +20595,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20441
20595
|
// Instead, we center them with css magic in styles.less
|
|
20442
20596
|
// /cry(aria)
|
|
20443
20597
|
this._foundTextNodes = true;
|
|
20444
|
-
if (
|
|
20598
|
+
if (this.widgetIds.includes(node.id)) {
|
|
20445
20599
|
// We don't want to render a duplicate widget key/ref,
|
|
20446
20600
|
// as this causes problems with react (for obvious
|
|
20447
20601
|
// reasons). Instead we just notify the
|
|
@@ -20766,7 +20920,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20766
20920
|
};
|
|
20767
20921
|
getInputPaths = () => {
|
|
20768
20922
|
const inputPaths = [];
|
|
20769
|
-
|
|
20923
|
+
this.widgetIds.forEach(widgetId => {
|
|
20770
20924
|
const widget = this.getWidgetInstance(widgetId);
|
|
20771
20925
|
if (widget && widget.getInputPaths) {
|
|
20772
20926
|
// Grab all input paths and add widgetID to the front
|
|
@@ -20774,7 +20928,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20774
20928
|
// Prefix paths with their widgetID and add to collective
|
|
20775
20929
|
// list of paths.
|
|
20776
20930
|
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
20777
|
-
|
|
20931
|
+
widgetInputPaths.forEach(inputPath => {
|
|
20778
20932
|
const relativeInputPath = [widgetId].concat(inputPath);
|
|
20779
20933
|
inputPaths.push(relativeInputPath);
|
|
20780
20934
|
});
|
|
@@ -20967,33 +21121,30 @@ class Renderer extends React__namespace.Component {
|
|
|
20967
21121
|
}
|
|
20968
21122
|
|
|
20969
21123
|
/**
|
|
20970
|
-
*
|
|
20971
|
-
*
|
|
20972
|
-
*
|
|
20973
|
-
*/
|
|
20974
|
-
scoreWidgets() {
|
|
20975
|
-
return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20976
|
-
}
|
|
20977
|
-
|
|
20978
|
-
/**
|
|
20979
|
-
* Grades the content.
|
|
21124
|
+
* Scores the content.
|
|
21125
|
+
*
|
|
21126
|
+
* @deprecated use scorePerseusItem
|
|
20980
21127
|
*/
|
|
20981
21128
|
score() {
|
|
20982
|
-
const scores = this.
|
|
21129
|
+
const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20983
21130
|
const combinedScore = Util.flattenScores(scores);
|
|
20984
21131
|
return combinedScore;
|
|
20985
21132
|
}
|
|
20986
|
-
|
|
21133
|
+
|
|
21134
|
+
/**
|
|
21135
|
+
* @deprecated use scorePerseusItem
|
|
21136
|
+
*/
|
|
21137
|
+
guessAndScore = () => {
|
|
20987
21138
|
const totalGuess = this.getUserInput();
|
|
20988
21139
|
const totalScore = this.score();
|
|
20989
21140
|
return [totalGuess, totalScore];
|
|
20990
|
-
}
|
|
21141
|
+
};
|
|
20991
21142
|
examples = () => {
|
|
20992
21143
|
const widgetIds = this.widgetIds;
|
|
20993
|
-
const examples =
|
|
21144
|
+
const examples = widgetIds.map(widgetId => {
|
|
20994
21145
|
const widget = this.getWidgetInstance(widgetId);
|
|
20995
21146
|
return widget != null && widget.examples ? widget.examples() : null;
|
|
20996
|
-
}));
|
|
21147
|
+
}).filter(Boolean);
|
|
20997
21148
|
|
|
20998
21149
|
// no widgets with examples
|
|
20999
21150
|
if (!examples.length) {
|
|
@@ -21376,6 +21527,7 @@ const HORIZONTAL_ALIGNMNENTS = {
|
|
|
21376
21527
|
* ```
|
|
21377
21528
|
*/
|
|
21378
21529
|
class Tooltip extends React__namespace.Component {
|
|
21530
|
+
tooltipContainerRef = /*#__PURE__*/React__namespace.createRef();
|
|
21379
21531
|
static defaultProps = {
|
|
21380
21532
|
className: "",
|
|
21381
21533
|
arrowSize: 10,
|
|
@@ -21458,8 +21610,7 @@ class Tooltip extends React__namespace.Component {
|
|
|
21458
21610
|
display: this.props.show ? "block" : "none"
|
|
21459
21611
|
}
|
|
21460
21612
|
}, /*#__PURE__*/React__namespace.createElement("div", {
|
|
21461
|
-
|
|
21462
|
-
ref: "tooltipContainer",
|
|
21613
|
+
ref: this.tooltipContainerRef,
|
|
21463
21614
|
className: "tooltipContainer",
|
|
21464
21615
|
style: {
|
|
21465
21616
|
position: "absolute",
|
|
@@ -21486,10 +21637,8 @@ class Tooltip extends React__namespace.Component {
|
|
|
21486
21637
|
}, this.props.children.slice(1)), arrowBelow));
|
|
21487
21638
|
}
|
|
21488
21639
|
_updateHeight() {
|
|
21489
|
-
const tooltipContainer =
|
|
21490
|
-
|
|
21491
|
-
this.refs.tooltipContainer);
|
|
21492
|
-
const height = tooltipContainer.offsetHeight;
|
|
21640
|
+
const tooltipContainer = this.tooltipContainerRef.current;
|
|
21641
|
+
const height = tooltipContainer?.offsetHeight || 0;
|
|
21493
21642
|
if (height !== this.state.height) {
|
|
21494
21643
|
this.setState({
|
|
21495
21644
|
height
|
|
@@ -27443,6 +27592,7 @@ function scoreDropdown(userInput, rubric) {
|
|
|
27443
27592
|
}
|
|
27444
27593
|
|
|
27445
27594
|
class Dropdown extends React__namespace.Component {
|
|
27595
|
+
static contextType = PerseusI18nContext;
|
|
27446
27596
|
static defaultProps = {
|
|
27447
27597
|
choices: [],
|
|
27448
27598
|
selected: 0,
|
|
@@ -27485,26 +27635,44 @@ class Dropdown extends React__namespace.Component {
|
|
|
27485
27635
|
value: String(i + 1),
|
|
27486
27636
|
label: choice
|
|
27487
27637
|
}))];
|
|
27488
|
-
return /*#__PURE__*/React__namespace.createElement(
|
|
27489
|
-
|
|
27490
|
-
|
|
27638
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
|
|
27639
|
+
scope: "dropdown-widget",
|
|
27640
|
+
mockOnFirstRender: true
|
|
27641
|
+
}, ids => /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
|
|
27642
|
+
// NOTE(jared): These are required to prevent weird behavior
|
|
27643
|
+
// When there's a dropdown in a zoomable table.
|
|
27644
|
+
, {
|
|
27491
27645
|
onClick: e => {
|
|
27492
27646
|
e.stopPropagation();
|
|
27493
27647
|
},
|
|
27494
27648
|
onTouchStart: e => {
|
|
27495
27649
|
e.stopPropagation();
|
|
27496
27650
|
}
|
|
27497
|
-
}, /*#__PURE__*/React__namespace.createElement(
|
|
27651
|
+
}, this.props.visibleLabel && /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelLarge, {
|
|
27652
|
+
tag: "label",
|
|
27653
|
+
id: ids.get("dropdown-label")
|
|
27654
|
+
}, this.props.visibleLabel), /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
|
|
27655
|
+
id: ids.get("dropdown"),
|
|
27498
27656
|
placeholder: "",
|
|
27499
27657
|
onChange: value => this._handleChange(parseInt(value)),
|
|
27500
27658
|
selectedValue: String(this.props.selected),
|
|
27501
|
-
disabled: this.props.apiOptions.readOnly
|
|
27502
|
-
|
|
27659
|
+
disabled: this.props.apiOptions.readOnly,
|
|
27660
|
+
"aria-label": this.props.ariaLabel || this.context.strings.selectAnAnswer,
|
|
27661
|
+
"aria-labelledby": ids.get("dropdown-label")
|
|
27662
|
+
// This is currently necessary for SRs to read the labels properly.
|
|
27663
|
+
// However, WB is working on a change to add the "combobox" role to
|
|
27664
|
+
// all dropdowns.
|
|
27665
|
+
// See https://khanacademy.atlassian.net/browse/WB-1671
|
|
27666
|
+
,
|
|
27667
|
+
role: "combobox"
|
|
27668
|
+
}, children)));
|
|
27503
27669
|
}
|
|
27504
27670
|
}
|
|
27505
27671
|
const optionsTransform = widgetOptions => {
|
|
27506
27672
|
return {
|
|
27507
27673
|
placeholder: widgetOptions.placeholder,
|
|
27674
|
+
visibleLabel: widgetOptions.visibleLabel,
|
|
27675
|
+
ariaLabel: widgetOptions.ariaLabel,
|
|
27508
27676
|
choices: widgetOptions.choices.map(choice => choice.content)
|
|
27509
27677
|
};
|
|
27510
27678
|
};
|
|
@@ -29584,6 +29752,9 @@ class Group extends React__namespace.Component {
|
|
|
29584
29752
|
this.change("widgets", this.rendererRef.props.widgets);
|
|
29585
29753
|
}
|
|
29586
29754
|
};
|
|
29755
|
+
|
|
29756
|
+
// TODO(LEMS-2391): replace this when there's a separate check
|
|
29757
|
+
// for valid/invalid state
|
|
29587
29758
|
const score = this.rendererRef?.score();
|
|
29588
29759
|
const isValid = score && score.type !== "invalid";
|
|
29589
29760
|
const isInvalid = score && score.type === "invalid";
|
|
@@ -30630,6 +30801,7 @@ function numSteps$1(range, step) {
|
|
|
30630
30801
|
return Math.floor((range[1] - range[0]) / step);
|
|
30631
30802
|
}
|
|
30632
30803
|
class Graph extends React__namespace.Component {
|
|
30804
|
+
graphieDivRef = /*#__PURE__*/React__namespace.createRef();
|
|
30633
30805
|
protractor;
|
|
30634
30806
|
ruler;
|
|
30635
30807
|
_graphie;
|
|
@@ -30709,9 +30881,7 @@ class Graph extends React__namespace.Component {
|
|
|
30709
30881
|
if (this._hasSetupGraphieThisUpdate) {
|
|
30710
30882
|
return;
|
|
30711
30883
|
}
|
|
30712
|
-
|
|
30713
|
-
// eslint-disable-next-line react/no-string-refs
|
|
30714
|
-
const graphieDiv = ReactDOM__default["default"].findDOMNode(this.refs.graphieDiv);
|
|
30884
|
+
const graphieDiv = this.graphieDivRef.current;
|
|
30715
30885
|
// @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'empty' does not exist on type 'JQueryStatic'.
|
|
30716
30886
|
$__default["default"](graphieDiv).empty();
|
|
30717
30887
|
|
|
@@ -30900,7 +31070,7 @@ class Graph extends React__namespace.Component {
|
|
|
30900
31070
|
onClick: this.onClick
|
|
30901
31071
|
}, image, /*#__PURE__*/React__namespace.createElement("div", {
|
|
30902
31072
|
className: "graphie",
|
|
30903
|
-
ref:
|
|
31073
|
+
ref: this.graphieDivRef
|
|
30904
31074
|
}));
|
|
30905
31075
|
}
|
|
30906
31076
|
}
|
|
@@ -31986,13 +32156,13 @@ class InfoTip extends React__namespace.Component {
|
|
|
31986
32156
|
* this component allows multiple selection!
|
|
31987
32157
|
*/
|
|
31988
32158
|
class MultiButtonGroup extends React__namespace.Component {
|
|
32159
|
+
buttonContainerRef = /*#__PURE__*/React__namespace.createRef();
|
|
31989
32160
|
static defaultProps = {
|
|
31990
32161
|
values: [],
|
|
31991
32162
|
allowEmpty: true
|
|
31992
32163
|
};
|
|
31993
32164
|
focus() {
|
|
31994
|
-
|
|
31995
|
-
ReactDOM__namespace.findDOMNode(this)?.focus();
|
|
32165
|
+
this.buttonContainerRef.current?.focus();
|
|
31996
32166
|
return true;
|
|
31997
32167
|
}
|
|
31998
32168
|
toggleSelect = newValue => {
|
|
@@ -32027,7 +32197,8 @@ class MultiButtonGroup extends React__namespace.Component {
|
|
|
32027
32197
|
display: "inline-block"
|
|
32028
32198
|
};
|
|
32029
32199
|
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
32030
|
-
style: outerStyle
|
|
32200
|
+
style: outerStyle,
|
|
32201
|
+
ref: this.buttonContainerRef
|
|
32031
32202
|
}, buttons);
|
|
32032
32203
|
}
|
|
32033
32204
|
}
|
|
@@ -45209,7 +45380,7 @@ const init = function () {
|
|
|
45209
45380
|
|
|
45210
45381
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45211
45382
|
const libName = "@khanacademy/perseus";
|
|
45212
|
-
const libVersion = "
|
|
45383
|
+
const libVersion = "45.0.0";
|
|
45213
45384
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45214
45385
|
|
|
45215
45386
|
const apiVersion = {
|
|
@@ -45813,6 +45984,13 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45813
45984
|
this.props.onRendered(true);
|
|
45814
45985
|
}
|
|
45815
45986
|
}
|
|
45987
|
+
if (this.props.score && this.props.score !== prevProps.score) {
|
|
45988
|
+
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45989
|
+
this.setState({
|
|
45990
|
+
questionCompleted: this.props.score.correct,
|
|
45991
|
+
questionHighlightedWidgets: emptyQuestionAreaWidgets
|
|
45992
|
+
});
|
|
45993
|
+
}
|
|
45816
45994
|
}
|
|
45817
45995
|
componentWillUnmount() {
|
|
45818
45996
|
if (this.blurTimeoutID != null) {
|
|
@@ -45948,18 +46126,37 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45948
46126
|
return this.questionRenderer.getPromptJSON();
|
|
45949
46127
|
}
|
|
45950
46128
|
|
|
46129
|
+
/**
|
|
46130
|
+
* Returns an array of the widget `.getUserInput()` results
|
|
46131
|
+
*
|
|
46132
|
+
* TODO: can we remove this? Seems to be just for backwards
|
|
46133
|
+
* compatibility with old Perseus Chrome logging
|
|
46134
|
+
* @deprecated use getUserInput
|
|
46135
|
+
*/
|
|
46136
|
+
getUserInputLegacy() {
|
|
46137
|
+
return this.questionRenderer.getUserInput();
|
|
46138
|
+
}
|
|
46139
|
+
|
|
46140
|
+
/**
|
|
46141
|
+
* Returns an object of the widget `.getUserInput()` results
|
|
46142
|
+
*/
|
|
46143
|
+
getUserInput() {
|
|
46144
|
+
return this.questionRenderer.getUserInputMap();
|
|
46145
|
+
}
|
|
46146
|
+
|
|
45951
46147
|
/**
|
|
45952
46148
|
* Grades the item.
|
|
46149
|
+
*
|
|
46150
|
+
* @deprecated use scorePerseusItem
|
|
45953
46151
|
*/
|
|
45954
46152
|
scoreInput() {
|
|
45955
|
-
const
|
|
45956
|
-
const
|
|
45957
|
-
const score = guessAndScore[1];
|
|
46153
|
+
const guess = this.getUserInput();
|
|
46154
|
+
const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
|
|
45958
46155
|
|
|
45959
46156
|
// Continue to include an empty guess for the now defunct answer area.
|
|
45960
46157
|
// TODO(alex): Check whether we rely on the format here for
|
|
45961
46158
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
45962
|
-
const maxCompatGuess = [
|
|
46159
|
+
const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
|
|
45963
46160
|
const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
45964
46161
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45965
46162
|
this.setState({
|
|
@@ -47821,138 +48018,6 @@ const registerAllWidgetsForTesting = () => {
|
|
|
47821
48018
|
replaceDeprecatedWidgets();
|
|
47822
48019
|
};
|
|
47823
48020
|
|
|
47824
|
-
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
47825
|
-
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
47826
|
-
|
|
47827
|
-
/**
|
|
47828
|
-
* Regex for widget placeholders in a string.
|
|
47829
|
-
*
|
|
47830
|
-
* First capture group is the widget ID (ex. 'radio 1')
|
|
47831
|
-
* Second capture group is the widget type (ex. "radio)
|
|
47832
|
-
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
47833
|
-
*/
|
|
47834
|
-
function getWidgetRegex() {
|
|
47835
|
-
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
47836
|
-
}
|
|
47837
|
-
|
|
47838
|
-
/**
|
|
47839
|
-
* Add a widget placeholder using the widget ID.
|
|
47840
|
-
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
47841
|
-
*
|
|
47842
|
-
* @param {string} id
|
|
47843
|
-
* @returns {string}
|
|
47844
|
-
*/
|
|
47845
|
-
function addWidget(id) {
|
|
47846
|
-
return `[[☃ ${id}]]`;
|
|
47847
|
-
}
|
|
47848
|
-
|
|
47849
|
-
/**
|
|
47850
|
-
* Get a widget type by a widget's ID
|
|
47851
|
-
* (please don't derive type from ID)
|
|
47852
|
-
*
|
|
47853
|
-
* @param {string} widgetId the ID of the widget
|
|
47854
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47855
|
-
* @returns {string} the widget type (ie "radio")
|
|
47856
|
-
*/
|
|
47857
|
-
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
47858
|
-
const widget = widgetMap[WidgetId];
|
|
47859
|
-
return widget?.type ?? null;
|
|
47860
|
-
}
|
|
47861
|
-
|
|
47862
|
-
/**
|
|
47863
|
-
* Does the content have a specific type of widget?
|
|
47864
|
-
*
|
|
47865
|
-
* @param {string} type the type of the widget in question (ie "radio")
|
|
47866
|
-
* @param {string} content the string to search through
|
|
47867
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47868
|
-
* @returns {boolean} if the content includes the widget type
|
|
47869
|
-
*/
|
|
47870
|
-
function contentHasWidgetType(type, content, widgetMap) {
|
|
47871
|
-
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
47872
|
-
}
|
|
47873
|
-
|
|
47874
|
-
/**
|
|
47875
|
-
* Extract all widget IDs, which includes the widget type and instance number.
|
|
47876
|
-
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
47877
|
-
*
|
|
47878
|
-
* Content should contain Perseus widget placeholders,
|
|
47879
|
-
* which look like: '[[☃ radio 1]]'.
|
|
47880
|
-
*
|
|
47881
|
-
* @param {string} content
|
|
47882
|
-
* @returns {ReadonlyArray<string>} widgetIds
|
|
47883
|
-
*/
|
|
47884
|
-
function getWidgetIdsFromContent(content) {
|
|
47885
|
-
const widgets = [];
|
|
47886
|
-
const localWidgetRegex = getWidgetRegex();
|
|
47887
|
-
let match = localWidgetRegex.exec(content);
|
|
47888
|
-
while (match !== null) {
|
|
47889
|
-
widgets.push(match[1]);
|
|
47890
|
-
match = localWidgetRegex.exec(content);
|
|
47891
|
-
}
|
|
47892
|
-
return widgets;
|
|
47893
|
-
}
|
|
47894
|
-
|
|
47895
|
-
/**
|
|
47896
|
-
* Get a list of widget IDs from content,
|
|
47897
|
-
* but only for specific widget types
|
|
47898
|
-
*
|
|
47899
|
-
* @param {string} type the type of widget (ie "radio")
|
|
47900
|
-
* @param {string} content the string to parse
|
|
47901
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47902
|
-
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
47903
|
-
*/
|
|
47904
|
-
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
47905
|
-
const rv = [];
|
|
47906
|
-
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
47907
|
-
widgetIdsInContent.forEach(widgetId => {
|
|
47908
|
-
const widget = widgetMap[widgetId];
|
|
47909
|
-
if (widget?.type === type) {
|
|
47910
|
-
rv.push(widgetId);
|
|
47911
|
-
}
|
|
47912
|
-
});
|
|
47913
|
-
return rv;
|
|
47914
|
-
}
|
|
47915
|
-
|
|
47916
|
-
/**
|
|
47917
|
-
* Pull the widget map out of ItemData
|
|
47918
|
-
*
|
|
47919
|
-
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
47920
|
-
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
47921
|
-
*/
|
|
47922
|
-
function getWidgetsMapFromItemData(itemData) {
|
|
47923
|
-
return itemData.question.widgets;
|
|
47924
|
-
}
|
|
47925
|
-
|
|
47926
|
-
/**
|
|
47927
|
-
* Get the widget information from a WidgetMap
|
|
47928
|
-
*
|
|
47929
|
-
* @param {string} widgetId the ID of the widget
|
|
47930
|
-
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
47931
|
-
*/
|
|
47932
|
-
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
47933
|
-
return widgetMap[widgetId] ?? null;
|
|
47934
|
-
}
|
|
47935
|
-
|
|
47936
|
-
/**
|
|
47937
|
-
* Get select widgets from a widget map.
|
|
47938
|
-
* Useful for multi-items that needs to split
|
|
47939
|
-
* one PerseusItem into several
|
|
47940
|
-
*
|
|
47941
|
-
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
47942
|
-
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
47943
|
-
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
47944
|
-
*/
|
|
47945
|
-
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
47946
|
-
const widgets = {};
|
|
47947
|
-
widgetIds.forEach(widgetId => {
|
|
47948
|
-
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
47949
|
-
if (widget) {
|
|
47950
|
-
widgets[widgetId] = widget;
|
|
47951
|
-
}
|
|
47952
|
-
});
|
|
47953
|
-
return widgets;
|
|
47954
|
-
}
|
|
47955
|
-
|
|
47956
48021
|
/**
|
|
47957
48022
|
* This function extracts the answers from the widgets.
|
|
47958
48023
|
*
|
|
@@ -48896,6 +48961,8 @@ const parseDefinitionWidget = parseWidget(constant("definition"), object({
|
|
|
48896
48961
|
|
|
48897
48962
|
const parseDropdownWidget = parseWidget(constant("dropdown"), object({
|
|
48898
48963
|
placeholder: string,
|
|
48964
|
+
ariaLabel: optional(string),
|
|
48965
|
+
visibleLabel: optional(string),
|
|
48899
48966
|
static: defaulted(boolean, () => false),
|
|
48900
48967
|
choices: array(object({
|
|
48901
48968
|
content: string,
|
|
@@ -50120,6 +50187,7 @@ exports.parsePerseusItem = parsePerseusItem;
|
|
|
50120
50187
|
exports.plotterPlotTypes = plotterPlotTypes;
|
|
50121
50188
|
exports.preprocessTex = preprocessTex;
|
|
50122
50189
|
exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
|
|
50190
|
+
exports.scorePerseusItem = scorePerseusItem;
|
|
50123
50191
|
exports.shapes = shapes;
|
|
50124
50192
|
exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
|
|
50125
50193
|
exports.traverse = traverse;
|