@khanacademy/perseus 42.0.3 → 43.0.1
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/svg-image.d.ts +1 -1
- package/dist/es/index.css +0 -3
- package/dist/es/index.js +536 -723
- package/dist/es/index.js.map +1 -1
- package/dist/es/strings.js +11 -3
- package/dist/es/strings.js.map +1 -1
- package/dist/index.css +0 -3
- package/dist/index.d.ts +3 -6
- package/dist/index.js +513 -703
- package/dist/index.js.map +1 -1
- package/dist/renderer-util.d.ts +2 -3
- package/dist/renderer.d.ts +9 -6
- package/dist/server-item-renderer.d.ts +2 -33
- package/dist/strings.d.ts +2 -0
- package/dist/strings.js +11 -3
- package/dist/strings.js.map +1 -1
- package/dist/types.d.ts +0 -5
- package/dist/validation.types.d.ts +31 -1
- package/dist/widgets/expression/expression.d.ts +3 -3
- package/dist/widgets/expression/validate-expression.d.ts +14 -0
- package/dist/widgets/group/group.d.ts +1 -1
- package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +13 -1
- package/dist/widgets/interactive-graphs/types.d.ts +1 -0
- package/dist/widgets/interactive-graphs/utils.d.ts +11 -1
- package/dist/widgets/matrix/validate-matrix.d.ts +15 -0
- package/dist/widgets/numeric-input/numeric-input.d.ts +0 -7
- package/package.json +5 -5
- package/dist/util/deprecated-widgets/input-number.d.ts +0 -13
- package/dist/util/deprecated-widgets/modernize-widgets-utils.d.ts +0 -12
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ var kmath = require('@khanacademy/kmath');
|
|
|
24
24
|
var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
|
|
25
25
|
var wonderBlocksProgressSpinner = require('@khanacademy/wonder-blocks-progress-spinner');
|
|
26
26
|
var SimpleMarkdown = require('@khanacademy/simple-markdown');
|
|
27
|
-
var
|
|
27
|
+
var pureMarkdown = require('@khanacademy/pure-markdown');
|
|
28
28
|
var wonderBlocksForm = require('@khanacademy/wonder-blocks-form');
|
|
29
29
|
var Button = require('@khanacademy/wonder-blocks-button');
|
|
30
30
|
var wonderBlocksLayout = require('@khanacademy/wonder-blocks-layout');
|
|
@@ -77,7 +77,6 @@ var Clickable__default = /*#__PURE__*/_interopDefaultLegacy(Clickable);
|
|
|
77
77
|
var $__default = /*#__PURE__*/_interopDefaultLegacy($);
|
|
78
78
|
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
|
|
79
79
|
var SimpleMarkdown__default = /*#__PURE__*/_interopDefaultLegacy(SimpleMarkdown);
|
|
80
|
-
var SimpleMarkdown__namespace = /*#__PURE__*/_interopNamespace(SimpleMarkdown$1);
|
|
81
80
|
var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
|
|
82
81
|
var Banner__default = /*#__PURE__*/_interopDefaultLegacy(Banner);
|
|
83
82
|
var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
|
|
@@ -369,6 +368,8 @@ const mockStrings = {
|
|
|
369
368
|
addPoint: "Add Point",
|
|
370
369
|
removePoint: "Remove Point",
|
|
371
370
|
graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
|
|
371
|
+
closePolygon: "Close shape",
|
|
372
|
+
openPolygon: "Re-open shape",
|
|
372
373
|
srPointAtCoordinates: _ref27 => {
|
|
373
374
|
let {
|
|
374
375
|
num,
|
|
@@ -530,7 +531,10 @@ class InnerMathInput extends React__namespace.Component {
|
|
|
530
531
|
locale
|
|
531
532
|
} = this.context;
|
|
532
533
|
// Initialize MathQuill.MathField instance
|
|
533
|
-
this.__mathField = mathInput.createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings,
|
|
534
|
+
this.__mathField = mathInput.createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings,
|
|
535
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
536
|
+
// @ts-expect-error: Type 'EditableMathQuill' is not assignable to type 'MathFieldInterface'.
|
|
537
|
+
baseConfig => ({
|
|
534
538
|
...baseConfig,
|
|
535
539
|
handlers: {
|
|
536
540
|
edit: debounce(mathField => {
|
|
@@ -2259,6 +2263,24 @@ const KhanAnswerTypes = {
|
|
|
2259
2263
|
}
|
|
2260
2264
|
};
|
|
2261
2265
|
|
|
2266
|
+
/**
|
|
2267
|
+
* Checks user input from the expression widget to see if it is scorable.
|
|
2268
|
+
*
|
|
2269
|
+
* Note: Most of the expression widget's validation requires the Rubric because
|
|
2270
|
+
* of its use of KhanAnswerTypes as a core part of scoring.
|
|
2271
|
+
*
|
|
2272
|
+
* @see `scoreExpression()` for more details.
|
|
2273
|
+
*/
|
|
2274
|
+
function validateExpression(userInput) {
|
|
2275
|
+
if (userInput === "") {
|
|
2276
|
+
return {
|
|
2277
|
+
type: "invalid",
|
|
2278
|
+
message: null
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
return null;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2262
2284
|
/* Content creators input a list of answers which are matched from top to
|
|
2263
2285
|
* bottom. The intent is that they can include spcific solutions which should
|
|
2264
2286
|
* be graded as correct or incorrect (or ungraded!) first, then get more
|
|
@@ -2278,6 +2300,10 @@ const KhanAnswerTypes = {
|
|
|
2278
2300
|
* - Otherwise, pass through the resulting points and message.
|
|
2279
2301
|
*/
|
|
2280
2302
|
function scoreExpression(userInput, rubric, strings, locale) {
|
|
2303
|
+
const validationError = validateExpression(userInput);
|
|
2304
|
+
if (validationError) {
|
|
2305
|
+
return validationError;
|
|
2306
|
+
}
|
|
2281
2307
|
const options = ___default["default"].clone(rubric);
|
|
2282
2308
|
___default["default"].extend(options, {
|
|
2283
2309
|
decimal_separator: getDecimalSeparator(locale)
|
|
@@ -2535,11 +2561,17 @@ class Expression extends React__namespace.Component {
|
|
|
2535
2561
|
}
|
|
2536
2562
|
return true;
|
|
2537
2563
|
};
|
|
2564
|
+
|
|
2565
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
2566
|
+
// @ts-expect-error: Type 'FocusPath' is not assignable to type 'InputPath'.
|
|
2538
2567
|
focusInputPath(inputPath) {
|
|
2539
2568
|
// eslint-disable-next-line react/no-string-refs
|
|
2540
2569
|
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
|
|
2541
2570
|
this.refs.input.focus();
|
|
2542
2571
|
}
|
|
2572
|
+
|
|
2573
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
2574
|
+
// @ts-expect-error: Type 'FocusPath' is not assignable to type 'InputPath'.
|
|
2543
2575
|
blurInputPath(inputPath) {
|
|
2544
2576
|
// eslint-disable-next-line react/no-string-refs
|
|
2545
2577
|
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
|
|
@@ -2713,7 +2745,7 @@ const keypadConfigurationForProps = widgetOptions => {
|
|
|
2713
2745
|
times: widgetOptions.times
|
|
2714
2746
|
};
|
|
2715
2747
|
};
|
|
2716
|
-
const propUpgrades$
|
|
2748
|
+
const propUpgrades$2 = {
|
|
2717
2749
|
/* c8 ignore next */
|
|
2718
2750
|
"1": v0props => ({
|
|
2719
2751
|
times: v0props.times,
|
|
@@ -2773,10 +2805,14 @@ var Expression$1 = {
|
|
|
2773
2805
|
major: 1,
|
|
2774
2806
|
minor: 0
|
|
2775
2807
|
},
|
|
2776
|
-
propUpgrades: propUpgrades$
|
|
2808
|
+
propUpgrades: propUpgrades$2,
|
|
2777
2809
|
// For use by the editor
|
|
2778
2810
|
isLintable: true,
|
|
2811
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
2812
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
|
|
2779
2813
|
scorer: scoreExpression,
|
|
2814
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
2815
|
+
// @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
|
|
2780
2816
|
getOneCorrectAnswerFromRubric(rubric) {
|
|
2781
2817
|
const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
|
|
2782
2818
|
if (correctAnswers.length === 0) {
|
|
@@ -2859,6 +2895,8 @@ const nestedMap$2 = function (children, func, context) {
|
|
|
2859
2895
|
return nestedMap$2(child, func);
|
|
2860
2896
|
});
|
|
2861
2897
|
}
|
|
2898
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
2899
|
+
// @ts-expect-error: T | ReadonlyArray<T> is not assignable to T
|
|
2862
2900
|
return func.call(context, children);
|
|
2863
2901
|
};
|
|
2864
2902
|
|
|
@@ -14637,7 +14675,9 @@ const constraints$2 = {
|
|
|
14637
14675
|
});
|
|
14638
14676
|
|
|
14639
14677
|
// bound the delta by the calculated bounds
|
|
14640
|
-
const boundedDelta = ___default["default"].reduce(deltaBounds,
|
|
14678
|
+
const boundedDelta = ___default["default"].reduce(deltaBounds,
|
|
14679
|
+
// @ts-expect-error: Type 'number[]' is not assignable to type 'Coord'.
|
|
14680
|
+
function (delta, bound) {
|
|
14641
14681
|
const lower = bound[0];
|
|
14642
14682
|
const upper = bound[1];
|
|
14643
14683
|
const deltaX = Math.max(lower[0], Math.min(upper[0], delta[0]));
|
|
@@ -15172,6 +15212,7 @@ const constraints$1 = {
|
|
|
15172
15212
|
};
|
|
15173
15213
|
},
|
|
15174
15214
|
snap: function (snap) {
|
|
15215
|
+
// @ts-expect-error: Type 'boolean | Coord' is not assignable to type 'Coord'.
|
|
15175
15216
|
return function (coord) {
|
|
15176
15217
|
if (snap === null) {
|
|
15177
15218
|
// TODO(benchristel), NOTE(kevinb): this should probably return
|
|
@@ -15194,6 +15235,8 @@ const constraints$1 = {
|
|
|
15194
15235
|
return optionalPaddingPx;
|
|
15195
15236
|
}
|
|
15196
15237
|
})();
|
|
15238
|
+
|
|
15239
|
+
// @ts-expect-error: Type 'boolean | Coord' is not assignable to type 'Coord'.
|
|
15197
15240
|
return function (coord, prev, options) {
|
|
15198
15241
|
const graphie = this.graphie;
|
|
15199
15242
|
range = range || graphie.range;
|
|
@@ -16097,15 +16140,15 @@ const constraints = {
|
|
|
16097
16140
|
});
|
|
16098
16141
|
|
|
16099
16142
|
// bound the delta by the calculated bounds
|
|
16100
|
-
const boundedDelta = ___default["default"].reduce(deltaBounds,
|
|
16143
|
+
const boundedDelta = ___default["default"].reduce(deltaBounds,
|
|
16144
|
+
// @ts-expect-error - TS2345 - Type 'number[]' is not assignable to type 'Coord'.
|
|
16145
|
+
function (delta, bound) {
|
|
16101
16146
|
const lower = bound[0];
|
|
16102
16147
|
const upper = bound[1];
|
|
16103
16148
|
const deltaX = Math.max(lower[0], Math.min(upper[0], delta[0]));
|
|
16104
16149
|
const deltaY = Math.max(lower[1], Math.min(upper[1], delta[1]));
|
|
16105
16150
|
return [deltaX, deltaY];
|
|
16106
16151
|
}, delta);
|
|
16107
|
-
|
|
16108
|
-
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'Coord'.
|
|
16109
16152
|
return kmath.vector.add(prevCoord, boundedDelta);
|
|
16110
16153
|
};
|
|
16111
16154
|
}
|
|
@@ -17022,6 +17065,7 @@ class ImageLoader extends React__namespace.Component {
|
|
|
17022
17065
|
|
|
17023
17066
|
this.img = new Image();
|
|
17024
17067
|
this.img.onload = this.handleLoad;
|
|
17068
|
+
// @ts-expect-error: Type 'string | Event' is not assignable to type 'Event'.
|
|
17025
17069
|
this.img.onerror = this.handleError;
|
|
17026
17070
|
this.img.src = this.props.src;
|
|
17027
17071
|
};
|
|
@@ -18080,6 +18124,7 @@ class InteractionTracker {
|
|
|
18080
18124
|
this.widgetType = widgetType;
|
|
18081
18125
|
this.widgetID = widgetID;
|
|
18082
18126
|
this.setting = setting;
|
|
18127
|
+
// @ts-expect-error: Type 'T | undefined' is not assignable to type 'T'.
|
|
18083
18128
|
this.track = this._track;
|
|
18084
18129
|
}
|
|
18085
18130
|
}
|
|
@@ -18514,10 +18559,10 @@ const styles$C = aphrodite.StyleSheet.create({
|
|
|
18514
18559
|
|
|
18515
18560
|
/* eslint-disable no-useless-escape, no-prototype-builtins */
|
|
18516
18561
|
const rules$1 = {
|
|
18517
|
-
...
|
|
18562
|
+
...pureMarkdown.pureMarkdownRules,
|
|
18518
18563
|
// NOTE: basically ignored by JIPT. wraps everything at the outer layer
|
|
18519
18564
|
columns: {
|
|
18520
|
-
...
|
|
18565
|
+
...pureMarkdown.pureMarkdownRules.columns,
|
|
18521
18566
|
react: (node, output, state) => {
|
|
18522
18567
|
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
18523
18568
|
className: "perseus-two-columns",
|
|
@@ -18545,7 +18590,7 @@ const rules$1 = {
|
|
|
18545
18590
|
// for exercises the renderer just renders the crowdin id to the
|
|
18546
18591
|
// renderer div.
|
|
18547
18592
|
crowdinId: {
|
|
18548
|
-
...
|
|
18593
|
+
...pureMarkdown.pureMarkdownRules.crowdinId,
|
|
18549
18594
|
react: (node, output, state) => node.id
|
|
18550
18595
|
},
|
|
18551
18596
|
// Ensure that the table is focusable in sequential keyboard navigation
|
|
@@ -18554,7 +18599,7 @@ const rules$1 = {
|
|
|
18554
18599
|
// left/right gesture. Note, setting `tabindex` doesn't appear required for
|
|
18555
18600
|
// iOS 14.4, in order for VoiceOver to work.
|
|
18556
18601
|
table: {
|
|
18557
|
-
...
|
|
18602
|
+
...pureMarkdown.pureMarkdownRules.table,
|
|
18558
18603
|
react: function (node, output, state) {
|
|
18559
18604
|
const table = SimpleMarkdown__default["default"].defaultRules.table.react(node, output, state);
|
|
18560
18605
|
if (!state.isMobile) {
|
|
@@ -18572,7 +18617,7 @@ const rules$1 = {
|
|
|
18572
18617
|
// our table regex into tableCapture[1..], and we pass tableCapture to
|
|
18573
18618
|
// our nptable regex
|
|
18574
18619
|
titledTable: {
|
|
18575
|
-
...
|
|
18620
|
+
...pureMarkdown.pureMarkdownRules.titledTable,
|
|
18576
18621
|
react: (node, output, state) => {
|
|
18577
18622
|
let contents;
|
|
18578
18623
|
if (!node.table) {
|
|
@@ -18632,7 +18677,7 @@ const rules$1 = {
|
|
|
18632
18677
|
}
|
|
18633
18678
|
},
|
|
18634
18679
|
widget: {
|
|
18635
|
-
...
|
|
18680
|
+
...pureMarkdown.pureMarkdownRules.widget,
|
|
18636
18681
|
react: (node, output, state) => {
|
|
18637
18682
|
// The actual output is handled in the renderer, where
|
|
18638
18683
|
// we know the current widget props/state. This is
|
|
@@ -18643,7 +18688,7 @@ const rules$1 = {
|
|
|
18643
18688
|
}
|
|
18644
18689
|
},
|
|
18645
18690
|
blockMath: {
|
|
18646
|
-
...
|
|
18691
|
+
...pureMarkdown.pureMarkdownRules.blockMath,
|
|
18647
18692
|
react: (node, output, state) => {
|
|
18648
18693
|
const {
|
|
18649
18694
|
TeX
|
|
@@ -18657,7 +18702,7 @@ const rules$1 = {
|
|
|
18657
18702
|
}
|
|
18658
18703
|
},
|
|
18659
18704
|
math: {
|
|
18660
|
-
...
|
|
18705
|
+
...pureMarkdown.pureMarkdownRules.math,
|
|
18661
18706
|
react: (node, output, state) => {
|
|
18662
18707
|
const {
|
|
18663
18708
|
TeX
|
|
@@ -18671,7 +18716,7 @@ const rules$1 = {
|
|
|
18671
18716
|
}
|
|
18672
18717
|
},
|
|
18673
18718
|
unescapedDollar: {
|
|
18674
|
-
...
|
|
18719
|
+
...pureMarkdown.pureMarkdownRules.unescapedDollar,
|
|
18675
18720
|
react: (node, output, state) => {
|
|
18676
18721
|
// Unescaped dollar signs render correctly, but result in
|
|
18677
18722
|
// untranslatable text after the i18n python linter flags it
|
|
@@ -18681,7 +18726,7 @@ const rules$1 = {
|
|
|
18681
18726
|
// Extend the SimpleMarkdown link parser to make the link open in a new
|
|
18682
18727
|
// window and handle reverse tabnapping phishing attacks
|
|
18683
18728
|
link: {
|
|
18684
|
-
...
|
|
18729
|
+
...pureMarkdown.pureMarkdownRules.link,
|
|
18685
18730
|
react: function (node, output, state) {
|
|
18686
18731
|
const link = SimpleMarkdown__default["default"].defaultRules.link.react(node, output, state);
|
|
18687
18732
|
const href = link.props.href;
|
|
@@ -18708,7 +18753,7 @@ const rules$1 = {
|
|
|
18708
18753
|
}
|
|
18709
18754
|
},
|
|
18710
18755
|
codeBlock: {
|
|
18711
|
-
...
|
|
18756
|
+
...pureMarkdown.pureMarkdownRules.codeBlock,
|
|
18712
18757
|
react: (node, output, state) => {
|
|
18713
18758
|
// ideally this should be a different rule, with only an
|
|
18714
18759
|
// output function, but right now that breaks the parser.
|
|
@@ -18726,7 +18771,7 @@ const rules$1 = {
|
|
|
18726
18771
|
// transform the tree to add lint nodes. This rule is here
|
|
18727
18772
|
// just for the react() function
|
|
18728
18773
|
lint: {
|
|
18729
|
-
...
|
|
18774
|
+
...pureMarkdown.pureMarkdownRules.lint,
|
|
18730
18775
|
react: (node, output, state) => {
|
|
18731
18776
|
return /*#__PURE__*/React__namespace.createElement(Lint, {
|
|
18732
18777
|
message: node.message,
|
|
@@ -18875,342 +18920,6 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18875
18920
|
}
|
|
18876
18921
|
}
|
|
18877
18922
|
|
|
18878
|
-
// Methods to convert input-number widgets to numeric-input widgets
|
|
18879
|
-
|
|
18880
|
-
const inputNumberToNumericInput = json => {
|
|
18881
|
-
// First we need to create a map of the old input-number keys to the new numeric-input keys
|
|
18882
|
-
// so that we can ensure we update the content and widgets accordingly
|
|
18883
|
-
const renameMap = getInputNumberRenameMap(json);
|
|
18884
|
-
|
|
18885
|
-
// Then we can use this to update the JSON
|
|
18886
|
-
return convertInputNumberJson(json, renameMap);
|
|
18887
|
-
};
|
|
18888
|
-
|
|
18889
|
-
// We need to be able to run this code recursively in order to convert nested input-number widgets
|
|
18890
|
-
const convertInputNumberJson = (json, renameMap) => {
|
|
18891
|
-
const updatedContent = convertDeprecatedWidgetsInContent(json, renameMap);
|
|
18892
|
-
const updatedWidgets = convertInputNumberWidgetOptions(json, renameMap);
|
|
18893
|
-
const modernizedJson = {
|
|
18894
|
-
...json,
|
|
18895
|
-
content: updatedContent,
|
|
18896
|
-
widgets: updatedWidgets
|
|
18897
|
-
};
|
|
18898
|
-
return modernizedJson;
|
|
18899
|
-
};
|
|
18900
|
-
|
|
18901
|
-
// Convert the input-number json in the widgets of a PerseusRenderer object
|
|
18902
|
-
const convertInputNumberWidgetOptions = (json, renameMap) => {
|
|
18903
|
-
const widgets = {
|
|
18904
|
-
...json.widgets
|
|
18905
|
-
};
|
|
18906
|
-
// The question.widgets is a dictionary map of widgets, so we need to loop through in order to convert input-number to numeric-input
|
|
18907
|
-
// which can exist as both the key or as a value on widget.type.
|
|
18908
|
-
for (const key of Object.keys(widgets)) {
|
|
18909
|
-
// Loop through the keys of the widgets dictionary
|
|
18910
|
-
if (widgets[key].options.widgets) {
|
|
18911
|
-
widgets[key].options = {
|
|
18912
|
-
...inputNumberToNumericInput(widgets[key].options)
|
|
18913
|
-
};
|
|
18914
|
-
}
|
|
18915
|
-
// Check if the widget is an input-number
|
|
18916
|
-
if (widgets[key].type === "input-number") {
|
|
18917
|
-
// If the answerType is not number or percent, we need to provide
|
|
18918
|
-
// the answer form for the numeric-input widget
|
|
18919
|
-
const provideAnswerForm = widgets[key].options.answerType !== "number" && widgets[key].options.answerType !== "percent";
|
|
18920
|
-
// We need to determine the mathFormat for the numeric-input widget
|
|
18921
|
-
const mathFormat = widgets[key].options.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
|
|
18922
|
-
: widgets[key].options.answerType; // Otherwise, we can use the answerType directly
|
|
18923
|
-
|
|
18924
|
-
// We need to update the answers prop to match the numeric-input widget format
|
|
18925
|
-
const answers = [{
|
|
18926
|
-
value: widgets[key].options.value,
|
|
18927
|
-
simplify: widgets[key].options.simplify,
|
|
18928
|
-
// Input Number doesn't have a strict prop, so we default to false
|
|
18929
|
-
strict: false,
|
|
18930
|
-
// We only want to set maxError if the inexact prop is true
|
|
18931
|
-
maxError: widgets[key].options.inexact ? widgets[key].options.maxError : 0,
|
|
18932
|
-
status: "correct",
|
|
18933
|
-
// Input-number only allows correct answers
|
|
18934
|
-
message: ""
|
|
18935
|
-
}];
|
|
18936
|
-
|
|
18937
|
-
// Add the required answerForms if provided/applicable
|
|
18938
|
-
if (provideAnswerForm) {
|
|
18939
|
-
answers[0].answerForms = [mathFormat];
|
|
18940
|
-
}
|
|
18941
|
-
|
|
18942
|
-
// Update the options prop to match the numeric-input widget format
|
|
18943
|
-
const upgradedWidgetOptions = {
|
|
18944
|
-
answers,
|
|
18945
|
-
size: widgets[key].options.size,
|
|
18946
|
-
coefficient: false,
|
|
18947
|
-
// input-number doesn't have a coefficient prop
|
|
18948
|
-
labelText: "",
|
|
18949
|
-
// input-number doesn't have a labelText prop
|
|
18950
|
-
static: false,
|
|
18951
|
-
// static is always false for numeric-input
|
|
18952
|
-
rightAlign: widgets[key].options.rightAlign || false
|
|
18953
|
-
};
|
|
18954
|
-
const upgradedWidget = {
|
|
18955
|
-
options: upgradedWidgetOptions,
|
|
18956
|
-
type: "numeric-input"
|
|
18957
|
-
};
|
|
18958
|
-
const newWidgetName = renameMap[key];
|
|
18959
|
-
// Create the new key entry
|
|
18960
|
-
widgets[newWidgetName] = upgradedWidget;
|
|
18961
|
-
|
|
18962
|
-
// We need to delete the old widget key
|
|
18963
|
-
delete widgets[key];
|
|
18964
|
-
}
|
|
18965
|
-
}
|
|
18966
|
-
return widgets;
|
|
18967
|
-
};
|
|
18968
|
-
|
|
18969
|
-
// Convert the deprecated widget refs in the content string
|
|
18970
|
-
// of a PerseusRenderer object to their renamed equivalents
|
|
18971
|
-
const convertDeprecatedWidgetsInContent = (json, renameMap) => {
|
|
18972
|
-
return Object.keys(renameMap).reduce((newContent, oldKey) => {
|
|
18973
|
-
const newKey = renameMap[oldKey];
|
|
18974
|
-
return newKey ? newContent.replace(oldKey, newKey) : newContent;
|
|
18975
|
-
}, json.content);
|
|
18976
|
-
};
|
|
18977
|
-
|
|
18978
|
-
// Create a map of the old input-number keys to the new numeric-input keys
|
|
18979
|
-
const getInputNumberRenameMap = json => {
|
|
18980
|
-
const numericRegex = /(?<=\[\[\u2603 )(numeric-input \d+)(?=\]\])/g;
|
|
18981
|
-
const inputNumberRegex = /(?<=\[\[\u2603 )(input-number \d+)(?=\]\])/g;
|
|
18982
|
-
|
|
18983
|
-
// Get all the content strings within the json, which might be nested within widgets
|
|
18984
|
-
const allContentStrings = json.content;
|
|
18985
|
-
|
|
18986
|
-
// Loop through the content strings to get all the input-number widgets
|
|
18987
|
-
const renameMap = {};
|
|
18988
|
-
const inputNumberMatches = [...(allContentStrings.match(inputNumberRegex) || [])];
|
|
18989
|
-
|
|
18990
|
-
// We want to count any pre-existing numeric-input widgets
|
|
18991
|
-
// so that we can start the new ids at the next number
|
|
18992
|
-
const numericMatches = [...(allContentStrings.match(numericRegex) || [])];
|
|
18993
|
-
let currentNumericCount = numericMatches.reduce((count, match) => {
|
|
18994
|
-
const id = parseInt(match.split(" ")[1], 10);
|
|
18995
|
-
return id >= count ? id + 1 : count; // We want to start at the next highest number
|
|
18996
|
-
}, 1);
|
|
18997
|
-
|
|
18998
|
-
// Now that we have all the necessary information, we can create the renameMap
|
|
18999
|
-
for (const match of inputNumberMatches) {
|
|
19000
|
-
const oldKey = match;
|
|
19001
|
-
const newKey = `numeric-input ${currentNumericCount}`;
|
|
19002
|
-
renameMap[oldKey] = newKey;
|
|
19003
|
-
currentNumericCount++;
|
|
19004
|
-
}
|
|
19005
|
-
return renameMap;
|
|
19006
|
-
};
|
|
19007
|
-
|
|
19008
|
-
// Convert the user input data keys from input-number to numeric-input
|
|
19009
|
-
const convertUserInputNumberData = (userInputMap, renameMap) => {
|
|
19010
|
-
const updatedUserInputMap = {
|
|
19011
|
-
...userInputMap
|
|
19012
|
-
};
|
|
19013
|
-
for (const key of Object.keys(userInputMap)) {
|
|
19014
|
-
if (key.includes("input-number")) {
|
|
19015
|
-
const updatedKey = renameMap[key];
|
|
19016
|
-
updatedUserInputMap[updatedKey] = userInputMap[key];
|
|
19017
|
-
delete updatedUserInputMap[key];
|
|
19018
|
-
}
|
|
19019
|
-
}
|
|
19020
|
-
return updatedUserInputMap;
|
|
19021
|
-
};
|
|
19022
|
-
|
|
19023
|
-
// Used to convert InputNumber widgets to NumericInput widgets for scoring
|
|
19024
|
-
const convertInputNumberForScoring = (rubric, userInputMap) => {
|
|
19025
|
-
// First we need to create a map of the old input-number keys to the new numeric-input keys
|
|
19026
|
-
// so that we can ensure we update the content, widgets, AND userInput accordingly
|
|
19027
|
-
const renameMap = getInputNumberRenameMap(rubric);
|
|
19028
|
-
const convertedRubric = convertInputNumberJson(rubric, renameMap);
|
|
19029
|
-
const convertedUserData = convertUserInputNumberData(userInputMap, renameMap);
|
|
19030
|
-
return {
|
|
19031
|
-
convertedRubric,
|
|
19032
|
-
convertedUserData
|
|
19033
|
-
};
|
|
19034
|
-
};
|
|
19035
|
-
|
|
19036
|
-
const widgetRegExes = [/input-number \d+/]; // We can add more regexes here in the future
|
|
19037
|
-
|
|
19038
|
-
// This utils in this file are used to modernize our Perseus JSON structure,
|
|
19039
|
-
// so that we can convert deprecated widgets to their modern equivalents when
|
|
19040
|
-
// content creators use the Editor Page to update content containing these widgets.
|
|
19041
|
-
// Currently, we're only converting input-number to numeric-input,
|
|
19042
|
-
// but we can add more conversions here in the future.
|
|
19043
|
-
|
|
19044
|
-
// Modernize the json content of a PerseusRenderer object
|
|
19045
|
-
// by converting deprecated widgets to their modern equivalents
|
|
19046
|
-
const convertDeprecatedWidgets = json => {
|
|
19047
|
-
// Currently we're only converting input-number to numeric-input,
|
|
19048
|
-
// But we can add more conversions here in the future
|
|
19049
|
-
return inputNumberToNumericInput(json);
|
|
19050
|
-
};
|
|
19051
|
-
const conversionRequired = json => {
|
|
19052
|
-
// If there's no content, then there's no conversion required
|
|
19053
|
-
if (!json.content) {
|
|
19054
|
-
return false;
|
|
19055
|
-
}
|
|
19056
|
-
// Check the content string for any top-level input-number widgets
|
|
19057
|
-
if (widgetRegExes.some(regex => regex.test(json.content))) {
|
|
19058
|
-
return true;
|
|
19059
|
-
}
|
|
19060
|
-
|
|
19061
|
-
// If there's no deprecated widget in the top-level, then check for any nested widgets
|
|
19062
|
-
for (const key of Object.keys(json.widgets)) {
|
|
19063
|
-
if (json.widgets[key].options.widgets) {
|
|
19064
|
-
const nestedJson = json.widgets[key].options;
|
|
19065
|
-
if (conversionRequired(nestedJson)) {
|
|
19066
|
-
return true;
|
|
19067
|
-
}
|
|
19068
|
-
}
|
|
19069
|
-
}
|
|
19070
|
-
return false;
|
|
19071
|
-
};
|
|
19072
|
-
|
|
19073
|
-
/**
|
|
19074
|
-
* Convert the deprecated widgets in the rubric and user data to their
|
|
19075
|
-
* modern equivalents for scoring. These need to be updated together
|
|
19076
|
-
* in order to ensure that the widgetKeys match between the rubric and user data.
|
|
19077
|
-
*/
|
|
19078
|
-
const convertDeprecatedWidgetsForScoring = (rubric, userInputMap) => {
|
|
19079
|
-
return convertInputNumberForScoring(rubric, userInputMap);
|
|
19080
|
-
};
|
|
19081
|
-
|
|
19082
|
-
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
19083
|
-
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
19084
|
-
|
|
19085
|
-
/**
|
|
19086
|
-
* Regex for widget placeholders in a string.
|
|
19087
|
-
*
|
|
19088
|
-
* First capture group is the widget ID (ex. 'radio 1')
|
|
19089
|
-
* Second capture group is the widget type (ex. "radio)
|
|
19090
|
-
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
19091
|
-
*/
|
|
19092
|
-
function getWidgetRegex() {
|
|
19093
|
-
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
19094
|
-
}
|
|
19095
|
-
|
|
19096
|
-
/**
|
|
19097
|
-
* Add a widget placeholder using the widget ID.
|
|
19098
|
-
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
19099
|
-
*
|
|
19100
|
-
* @param {string} id
|
|
19101
|
-
* @returns {string}
|
|
19102
|
-
*/
|
|
19103
|
-
function addWidget(id) {
|
|
19104
|
-
return `[[☃ ${id}]]`;
|
|
19105
|
-
}
|
|
19106
|
-
|
|
19107
|
-
/**
|
|
19108
|
-
* Get a widget type by a widget's ID
|
|
19109
|
-
* (please don't derive type from ID)
|
|
19110
|
-
*
|
|
19111
|
-
* @param {string} widgetId the ID of the widget
|
|
19112
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19113
|
-
* @returns {string} the widget type (ie "radio")
|
|
19114
|
-
*/
|
|
19115
|
-
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
19116
|
-
const widget = widgetMap[WidgetId];
|
|
19117
|
-
return widget?.type ?? null;
|
|
19118
|
-
}
|
|
19119
|
-
|
|
19120
|
-
/**
|
|
19121
|
-
* Does the content have a specific type of widget?
|
|
19122
|
-
*
|
|
19123
|
-
* @param {string} type the type of the widget in question (ie "radio")
|
|
19124
|
-
* @param {string} content the string to search through
|
|
19125
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19126
|
-
* @returns {boolean} if the content includes the widget type
|
|
19127
|
-
*/
|
|
19128
|
-
function contentHasWidgetType(type, content, widgetMap) {
|
|
19129
|
-
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
19130
|
-
}
|
|
19131
|
-
|
|
19132
|
-
/**
|
|
19133
|
-
* Extract all widget IDs, which includes the widget type and instance number.
|
|
19134
|
-
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
19135
|
-
*
|
|
19136
|
-
* Content should contain Perseus widget placeholders,
|
|
19137
|
-
* which look like: '[[☃ radio 1]]'.
|
|
19138
|
-
*
|
|
19139
|
-
* @param {string} content
|
|
19140
|
-
* @returns {ReadonlyArray<string>} widgetIds
|
|
19141
|
-
*/
|
|
19142
|
-
function getWidgetIdsFromContent(content) {
|
|
19143
|
-
const widgets = [];
|
|
19144
|
-
const localWidgetRegex = getWidgetRegex();
|
|
19145
|
-
let match = localWidgetRegex.exec(content);
|
|
19146
|
-
while (match !== null) {
|
|
19147
|
-
widgets.push(match[1]);
|
|
19148
|
-
match = localWidgetRegex.exec(content);
|
|
19149
|
-
}
|
|
19150
|
-
return widgets;
|
|
19151
|
-
}
|
|
19152
|
-
|
|
19153
|
-
/**
|
|
19154
|
-
* Get a list of widget IDs from content,
|
|
19155
|
-
* but only for specific widget types
|
|
19156
|
-
*
|
|
19157
|
-
* @param {string} type the type of widget (ie "radio")
|
|
19158
|
-
* @param {string} content the string to parse
|
|
19159
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19160
|
-
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
19161
|
-
*/
|
|
19162
|
-
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
19163
|
-
const rv = [];
|
|
19164
|
-
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
19165
|
-
widgetIdsInContent.forEach(widgetId => {
|
|
19166
|
-
const widget = widgetMap[widgetId];
|
|
19167
|
-
if (widget?.type === type) {
|
|
19168
|
-
rv.push(widgetId);
|
|
19169
|
-
}
|
|
19170
|
-
});
|
|
19171
|
-
return rv;
|
|
19172
|
-
}
|
|
19173
|
-
|
|
19174
|
-
/**
|
|
19175
|
-
* Pull the widget map out of ItemData
|
|
19176
|
-
*
|
|
19177
|
-
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
19178
|
-
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
19179
|
-
*/
|
|
19180
|
-
function getWidgetsMapFromItemData(itemData) {
|
|
19181
|
-
return itemData.question.widgets;
|
|
19182
|
-
}
|
|
19183
|
-
|
|
19184
|
-
/**
|
|
19185
|
-
* Get the widget information from a WidgetMap
|
|
19186
|
-
*
|
|
19187
|
-
* @param {string} widgetId the ID of the widget
|
|
19188
|
-
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
19189
|
-
*/
|
|
19190
|
-
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
19191
|
-
return widgetMap[widgetId] ?? null;
|
|
19192
|
-
}
|
|
19193
|
-
|
|
19194
|
-
/**
|
|
19195
|
-
* Get select widgets from a widget map.
|
|
19196
|
-
* Useful for multi-items that needs to split
|
|
19197
|
-
* one PerseusItem into several
|
|
19198
|
-
*
|
|
19199
|
-
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
19200
|
-
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
19201
|
-
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
19202
|
-
*/
|
|
19203
|
-
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
19204
|
-
const widgets = {};
|
|
19205
|
-
widgetIds.forEach(widgetId => {
|
|
19206
|
-
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
19207
|
-
if (widget) {
|
|
19208
|
-
widgets[widgetId] = widget;
|
|
19209
|
-
}
|
|
19210
|
-
});
|
|
19211
|
-
return widgets;
|
|
19212
|
-
}
|
|
19213
|
-
|
|
19214
18923
|
const DEFAULT_ALIGNMENT$1 = "block";
|
|
19215
18924
|
// NOTE(kevinb): "default" is not one in `validAlignments`.
|
|
19216
18925
|
const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
|
|
@@ -19261,9 +18970,6 @@ const replaceDeprecatedWidgets = () => {
|
|
|
19261
18970
|
replaceWidget("sequence", "deprecated-standin");
|
|
19262
18971
|
replaceWidget("simulator", "deprecated-standin");
|
|
19263
18972
|
replaceWidget("unit-input", "deprecated-standin");
|
|
19264
|
-
|
|
19265
|
-
// Input-Number is a special case as it is being replaced by Numeric-Input
|
|
19266
|
-
replaceWidget("input-number", "numeric-input");
|
|
19267
18973
|
};
|
|
19268
18974
|
const registerEditors = editorsToRegister => {
|
|
19269
18975
|
editorsToRegister.forEach(editor => {
|
|
@@ -19298,11 +19004,6 @@ const replaceDeprecatedEditors = () => {
|
|
|
19298
19004
|
replaceEditor("sequence", "deprecated-standin");
|
|
19299
19005
|
replaceEditor("simulator", "deprecated-standin");
|
|
19300
19006
|
replaceEditor("unit-input", "deprecated-standin");
|
|
19301
|
-
|
|
19302
|
-
// We're replacing the input-number editor with the numeric-input editor here,
|
|
19303
|
-
// but we're also modifying the JSON content to convert input-number into the
|
|
19304
|
-
// numeric-input format over in editor-page.tsx on line 92.
|
|
19305
|
-
replaceEditor("input-number", "numeric-input");
|
|
19306
19007
|
};
|
|
19307
19008
|
const getWidget = name => {
|
|
19308
19009
|
// TODO(alex): Consider referring to these as renderers to avoid
|
|
@@ -19349,8 +19050,10 @@ const getVersionVector = () => {
|
|
|
19349
19050
|
const getPublicWidgets = () => {
|
|
19350
19051
|
// TODO(alex): Update underscore.js so that _.pick can take a function.
|
|
19351
19052
|
// @ts-expect-error - TS2740 - Type 'Pick<{ [key: string]: Readonly<{ name: string; displayName: string; getWidget?: (() => ComponentType<any>) | undefined; accessible?: boolean | ((props: any) => boolean) | undefined; hidden?: boolean | undefined; ... 10 more ...; widget: ComponentType<...>; }>; }, string>' is missing the following properties from type 'readonly Readonly<{ name: string; displayName: string; getWidget?: (() => ComponentType<any>) | undefined; accessible?: boolean | ((props: any) => boolean) | undefined; hidden?: boolean | undefined; ... 10 more ...; widget: ComponentType<...>; }>[]': length, concat, join, slice, and 18 more.
|
|
19352
|
-
return ___default["default"].pick(widgets,
|
|
19353
|
-
|
|
19053
|
+
return ___default["default"].pick(widgets,
|
|
19054
|
+
// @ts-expect-error - TS2345 - Argument of type '(name: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
|
|
19055
|
+
___default["default"].reject(___default["default"].keys(widgets), function (name) {
|
|
19056
|
+
return widgets[name].hidden;
|
|
19354
19057
|
}));
|
|
19355
19058
|
};
|
|
19356
19059
|
const isAccessible = widgetInfo => {
|
|
@@ -19671,34 +19374,6 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19671
19374
|
}
|
|
19672
19375
|
});
|
|
19673
19376
|
}
|
|
19674
|
-
|
|
19675
|
-
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
19676
|
-
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
19677
|
-
function scorePerseusItem(perseusRenderData, userInputMap,
|
|
19678
|
-
// TODO(LEMS-2461,LEMS-2391): these probably
|
|
19679
|
-
// need to be removed before we move this to the server
|
|
19680
|
-
strings, locale) {
|
|
19681
|
-
let convertedRenderData = perseusRenderData;
|
|
19682
|
-
let convertedUserInputMap = userInputMap;
|
|
19683
|
-
|
|
19684
|
-
// Check if the PerseusRenderer object contains any deprecated widgets that need to be converted
|
|
19685
|
-
const mustConvertData = conversionRequired(perseusRenderData);
|
|
19686
|
-
if (mustConvertData) {
|
|
19687
|
-
const {
|
|
19688
|
-
convertedRubric,
|
|
19689
|
-
convertedUserData
|
|
19690
|
-
} = convertDeprecatedWidgetsForScoring(perseusRenderData, userInputMap);
|
|
19691
|
-
convertedRenderData = convertedRubric;
|
|
19692
|
-
convertedUserInputMap = convertedUserData;
|
|
19693
|
-
}
|
|
19694
|
-
|
|
19695
|
-
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
19696
|
-
// widget data for widgets that are not in PerseusRenderer.content,
|
|
19697
|
-
// so this checks that the widgets are being used before scoring them
|
|
19698
|
-
const usedWidgetIds = getWidgetIdsFromContent(convertedRenderData.content);
|
|
19699
|
-
const scores = scoreWidgetsFunctional(convertedRenderData.widgets, usedWidgetIds, convertedUserInputMap, strings, locale);
|
|
19700
|
-
return Util.flattenScores(scores);
|
|
19701
|
-
}
|
|
19702
19377
|
function scoreWidgetsFunctional(widgets,
|
|
19703
19378
|
// This is a port of old code, I'm not sure why
|
|
19704
19379
|
// we need widgetIds vs the keys of the widgets object
|
|
@@ -19719,7 +19394,7 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19719
19394
|
const userInput = userInputMap[id];
|
|
19720
19395
|
const scorer = getWidgetScorer(widget.type);
|
|
19721
19396
|
if (widget.type === "group") {
|
|
19722
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets,
|
|
19397
|
+
const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
|
|
19723
19398
|
widgetScores[id] = Util.flattenScores(scores);
|
|
19724
19399
|
} else if (scorer) {
|
|
19725
19400
|
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
@@ -20768,7 +20443,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20768
20443
|
// Instead, we center them with css magic in styles.less
|
|
20769
20444
|
// /cry(aria)
|
|
20770
20445
|
this._foundTextNodes = true;
|
|
20771
|
-
if (this.widgetIds
|
|
20446
|
+
if (___default["default"].contains(this.widgetIds, node.id)) {
|
|
20772
20447
|
// We don't want to render a duplicate widget key/ref,
|
|
20773
20448
|
// as this causes problems with react (for obvious
|
|
20774
20449
|
// reasons). Instead we just notify the
|
|
@@ -21093,7 +20768,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21093
20768
|
};
|
|
21094
20769
|
getInputPaths = () => {
|
|
21095
20770
|
const inputPaths = [];
|
|
21096
|
-
this.widgetIds
|
|
20771
|
+
___default["default"].each(this.widgetIds, widgetId => {
|
|
21097
20772
|
const widget = this.getWidgetInstance(widgetId);
|
|
21098
20773
|
if (widget && widget.getInputPaths) {
|
|
21099
20774
|
// Grab all input paths and add widgetID to the front
|
|
@@ -21101,7 +20776,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21101
20776
|
// Prefix paths with their widgetID and add to collective
|
|
21102
20777
|
// list of paths.
|
|
21103
20778
|
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
21104
|
-
|
|
20779
|
+
___default["default"].each(widgetInputPaths, inputPath => {
|
|
21105
20780
|
const relativeInputPath = [widgetId].concat(inputPath);
|
|
21106
20781
|
inputPaths.push(relativeInputPath);
|
|
21107
20782
|
});
|
|
@@ -21294,30 +20969,33 @@ class Renderer extends React__namespace.Component {
|
|
|
21294
20969
|
}
|
|
21295
20970
|
|
|
21296
20971
|
/**
|
|
21297
|
-
*
|
|
21298
|
-
*
|
|
21299
|
-
*
|
|
20972
|
+
* Returns an object mapping from widget ID to perseus-style score.
|
|
20973
|
+
* The keys of this object are the values of the array returned
|
|
20974
|
+
* from `getWidgetIds`.
|
|
21300
20975
|
*/
|
|
21301
|
-
|
|
21302
|
-
|
|
21303
|
-
const combinedScore = Util.flattenScores(scores);
|
|
21304
|
-
return combinedScore;
|
|
20976
|
+
scoreWidgets() {
|
|
20977
|
+
return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
21305
20978
|
}
|
|
21306
20979
|
|
|
21307
20980
|
/**
|
|
21308
|
-
*
|
|
20981
|
+
* Grades the content.
|
|
21309
20982
|
*/
|
|
21310
|
-
|
|
20983
|
+
score() {
|
|
20984
|
+
const scores = this.scoreWidgets();
|
|
20985
|
+
const combinedScore = Util.flattenScores(scores);
|
|
20986
|
+
return combinedScore;
|
|
20987
|
+
}
|
|
20988
|
+
guessAndScore() {
|
|
21311
20989
|
const totalGuess = this.getUserInput();
|
|
21312
20990
|
const totalScore = this.score();
|
|
21313
20991
|
return [totalGuess, totalScore];
|
|
21314
|
-
}
|
|
20992
|
+
}
|
|
21315
20993
|
examples = () => {
|
|
21316
20994
|
const widgetIds = this.widgetIds;
|
|
21317
|
-
const examples =
|
|
20995
|
+
const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
|
|
21318
20996
|
const widget = this.getWidgetInstance(widgetId);
|
|
21319
20997
|
return widget != null && widget.examples ? widget.examples() : null;
|
|
21320
|
-
})
|
|
20998
|
+
}));
|
|
21321
20999
|
|
|
21322
21000
|
// no widgets with examples
|
|
21323
21001
|
if (!examples.length) {
|
|
@@ -22322,11 +22000,17 @@ class InputNumber extends React__namespace.Component {
|
|
|
22322
22000
|
this.refs.input.focus();
|
|
22323
22001
|
return true;
|
|
22324
22002
|
};
|
|
22003
|
+
|
|
22004
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
22005
|
+
// @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
|
|
22325
22006
|
focusInputPath = inputPath => {
|
|
22326
22007
|
// eslint-disable-next-line react/no-string-refs
|
|
22327
22008
|
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
|
|
22328
22009
|
this.refs.input.focus();
|
|
22329
22010
|
};
|
|
22011
|
+
|
|
22012
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
22013
|
+
// @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
|
|
22330
22014
|
blurInputPath = inputPath => {
|
|
22331
22015
|
// eslint-disable-next-line react/no-string-refs
|
|
22332
22016
|
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
|
|
@@ -22342,6 +22026,9 @@ class InputNumber extends React__namespace.Component {
|
|
|
22342
22026
|
/* c8 ignore next */
|
|
22343
22027
|
return [[]];
|
|
22344
22028
|
};
|
|
22029
|
+
|
|
22030
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
22031
|
+
// @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
|
|
22345
22032
|
setInputValue = (path, newValue, cb) => {
|
|
22346
22033
|
this.props.onChange({
|
|
22347
22034
|
currentValue: newValue
|
|
@@ -22452,6 +22139,8 @@ var InputNumber$1 = {
|
|
|
22452
22139
|
widget: InputNumber,
|
|
22453
22140
|
transform: propTransform$3,
|
|
22454
22141
|
isLintable: true,
|
|
22142
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
22143
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
|
|
22455
22144
|
scorer: scoreInputNumber,
|
|
22456
22145
|
getOneCorrectAnswerFromRubric(rubric) {
|
|
22457
22146
|
if (rubric.value == null) {
|
|
@@ -22745,7 +22434,7 @@ class NumericInput extends React__namespace.Component {
|
|
|
22745
22434
|
height: "auto"
|
|
22746
22435
|
}
|
|
22747
22436
|
});
|
|
22748
|
-
return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
|
|
22437
|
+
return /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
|
|
22749
22438
|
ref: ref => this.inputRef = ref,
|
|
22750
22439
|
value: this.props.currentValue,
|
|
22751
22440
|
onChange: this.handleChange,
|
|
@@ -22757,7 +22446,7 @@ class NumericInput extends React__namespace.Component {
|
|
|
22757
22446
|
id: this.props.widgetId,
|
|
22758
22447
|
disabled: this.props.apiOptions.readOnly,
|
|
22759
22448
|
style: styles.input
|
|
22760
|
-
});
|
|
22449
|
+
}));
|
|
22761
22450
|
}
|
|
22762
22451
|
}
|
|
22763
22452
|
|
|
@@ -22815,69 +22504,19 @@ const propsTransform = function (widgetOptions) {
|
|
|
22815
22504
|
});
|
|
22816
22505
|
return rendererProps;
|
|
22817
22506
|
};
|
|
22818
|
-
|
|
22819
|
-
// This function is being used to replace the input-number widget
|
|
22820
|
-
// with the numeric-input widget
|
|
22821
|
-
const propUpgrades$2 = {
|
|
22822
|
-
/* c8 ignore next */
|
|
22823
|
-
"1": initialProps => {
|
|
22824
|
-
// If the initialProps has simplify, it means we're upgrading from
|
|
22825
|
-
// input-number to numeric-input. In this case, we need to upgrade
|
|
22826
|
-
// the widget options accordingly.
|
|
22827
|
-
if (initialProps.simplify !== undefined) {
|
|
22828
|
-
// If the answerType is not number or percent, we need to provide
|
|
22829
|
-
// the answer form for the numeric-input widget
|
|
22830
|
-
const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
|
|
22831
|
-
|
|
22832
|
-
// We need to determine the mathFormat for the numeric-input widget
|
|
22833
|
-
const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
|
|
22834
|
-
: initialProps.answerType; // Otherwise, we can use the answerType directly
|
|
22835
|
-
|
|
22836
|
-
// If adjusting this logic, also adjust the logic in the convertInputNumberWidgetOptions
|
|
22837
|
-
// function in input-number.ts in the Perseus Editor package's util folder
|
|
22838
|
-
const answers = [{
|
|
22839
|
-
value: initialProps.value,
|
|
22840
|
-
simplify: initialProps.simplify,
|
|
22841
|
-
answerForms: provideAnswerForm ? [mathFormat] : undefined,
|
|
22842
|
-
strict: initialProps.inexact,
|
|
22843
|
-
// We only want to set maxError if the inexact prop is true
|
|
22844
|
-
maxError: initialProps.inexact ? initialProps.maxError : 0,
|
|
22845
|
-
status: "correct",
|
|
22846
|
-
// Input-number only allows correct answers
|
|
22847
|
-
message: ""
|
|
22848
|
-
}];
|
|
22849
|
-
return {
|
|
22850
|
-
answers,
|
|
22851
|
-
size: initialProps.size,
|
|
22852
|
-
coefficient: false,
|
|
22853
|
-
// input-number doesn't have a coefficient prop
|
|
22854
|
-
labelText: "",
|
|
22855
|
-
// input-number doesn't have a labelText prop
|
|
22856
|
-
static: false,
|
|
22857
|
-
// static is always false for numeric-input
|
|
22858
|
-
rightAlign: initialProps.rightAlign || false
|
|
22859
|
-
};
|
|
22860
|
-
} else {
|
|
22861
|
-
// Otherwise simply return the initialProps as there's no differences
|
|
22862
|
-
// between v0 and v1 for numeric-input
|
|
22863
|
-
return initialProps;
|
|
22864
|
-
}
|
|
22865
|
-
}
|
|
22866
|
-
};
|
|
22867
22507
|
var NumericInput$1 = {
|
|
22868
22508
|
name: "numeric-input",
|
|
22869
22509
|
displayName: "Numeric input",
|
|
22870
22510
|
defaultAlignment: "inline-block",
|
|
22871
22511
|
accessible: true,
|
|
22872
22512
|
widget: NumericInput,
|
|
22873
|
-
version: {
|
|
22874
|
-
major: 1,
|
|
22875
|
-
minor: 0
|
|
22876
|
-
},
|
|
22877
22513
|
transform: propsTransform,
|
|
22878
|
-
propUpgrades: propUpgrades$2,
|
|
22879
22514
|
isLintable: true,
|
|
22515
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
22516
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
|
|
22880
22517
|
scorer: scoreNumericInput,
|
|
22518
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
22519
|
+
// @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
|
|
22881
22520
|
getOneCorrectAnswerFromRubric(rubric) {
|
|
22882
22521
|
const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
|
|
22883
22522
|
const answerStrings = correctAnswers.map(answer => {
|
|
@@ -24696,6 +24335,9 @@ const CIRCLE_LABEL_STYLE = {
|
|
|
24696
24335
|
borderRadius: "50%",
|
|
24697
24336
|
textAlign: "center"
|
|
24698
24337
|
};
|
|
24338
|
+
|
|
24339
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
24340
|
+
// @ts-expect-error: State is not assignable to ParseState
|
|
24699
24341
|
const builtParser = SimpleMarkdown__default["default"].parserFor(rules);
|
|
24700
24342
|
const parse$2 = (source, state) => {
|
|
24701
24343
|
// @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
|
|
@@ -27151,6 +26793,8 @@ var Radio = {
|
|
|
27151
26793
|
},
|
|
27152
26794
|
propUpgrades: propUpgrades$1,
|
|
27153
26795
|
isLintable: true,
|
|
26796
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
26797
|
+
// @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
|
|
27154
26798
|
scorer: scoreRadio
|
|
27155
26799
|
};
|
|
27156
26800
|
|
|
@@ -27374,6 +27018,8 @@ var Categorizer$1 = {
|
|
|
27374
27018
|
return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
|
|
27375
27019
|
},
|
|
27376
27020
|
isLintable: true,
|
|
27021
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
27022
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
|
|
27377
27023
|
scorer: scoreCategorizer
|
|
27378
27024
|
};
|
|
27379
27025
|
|
|
@@ -27611,6 +27257,8 @@ var CSProgram$1 = {
|
|
|
27611
27257
|
supportedAlignments: ["block", "full-width"],
|
|
27612
27258
|
widget: CSProgram,
|
|
27613
27259
|
hidden: true,
|
|
27260
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
27261
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
|
|
27614
27262
|
scorer: scoreCSProgram
|
|
27615
27263
|
};
|
|
27616
27264
|
|
|
@@ -27826,6 +27474,8 @@ var Dropdown$1 = {
|
|
|
27826
27474
|
accessible: true,
|
|
27827
27475
|
widget: Dropdown,
|
|
27828
27476
|
transform: optionsTransform,
|
|
27477
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
27478
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
|
|
27829
27479
|
scorer: scoreDropdown
|
|
27830
27480
|
};
|
|
27831
27481
|
|
|
@@ -29412,6 +29062,8 @@ class FunctionGrapher extends React__namespace.Component {
|
|
|
29412
29062
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
29413
29063
|
args[_key] = arguments[_key];
|
|
29414
29064
|
}
|
|
29065
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29066
|
+
// @ts-expect-error: Argument of type 'any[]' is not assignable to parameter of type '[newPropsOrSinglePropName: string | { [key: string]: any; }, propValue?: any, callback?: (() => unknown) | undefined]'. Target requires 1 element(s) but source may have fewer.
|
|
29415
29067
|
return change.apply(_this, args);
|
|
29416
29068
|
};
|
|
29417
29069
|
})();
|
|
@@ -29769,6 +29421,8 @@ var Grapher$1 = {
|
|
|
29769
29421
|
widget: Grapher,
|
|
29770
29422
|
transform: propTransform$2,
|
|
29771
29423
|
staticTransform: staticTransform$4,
|
|
29424
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29425
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
|
|
29772
29426
|
scorer: scoreGrapher
|
|
29773
29427
|
};
|
|
29774
29428
|
|
|
@@ -29892,9 +29546,6 @@ class Group extends React__namespace.Component {
|
|
|
29892
29546
|
this.change("widgets", this.rendererRef.props.widgets);
|
|
29893
29547
|
}
|
|
29894
29548
|
};
|
|
29895
|
-
|
|
29896
|
-
// TODO(LEMS-2391): replace this when there's a separate check
|
|
29897
|
-
// for valid/invalid state
|
|
29898
29549
|
const score = this.rendererRef?.score();
|
|
29899
29550
|
const isValid = score && score.type !== "invalid";
|
|
29900
29551
|
const isInvalid = score && score.type === "invalid";
|
|
@@ -30102,6 +29753,8 @@ var Iframe$1 = {
|
|
|
30102
29753
|
widget: Iframe,
|
|
30103
29754
|
// Let's not expose it to all content creators yet
|
|
30104
29755
|
hidden: true,
|
|
29756
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29757
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
|
|
30105
29758
|
scorer: scoreIframe
|
|
30106
29759
|
};
|
|
30107
29760
|
|
|
@@ -31779,6 +31432,38 @@ function isUnlimitedGraphState(state) {
|
|
|
31779
31432
|
return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
|
|
31780
31433
|
}
|
|
31781
31434
|
|
|
31435
|
+
/**
|
|
31436
|
+
* Parse a string of text and math into a list of objects with type and content
|
|
31437
|
+
*
|
|
31438
|
+
* Example: "Pi is about $\frac{22}{7}$" ==>
|
|
31439
|
+
* [
|
|
31440
|
+
* {type: "text", content: "Pi is about "},
|
|
31441
|
+
* {type: "math", content: "\\frac{22}{7}"},
|
|
31442
|
+
* ]
|
|
31443
|
+
*/
|
|
31444
|
+
const mathOnlyParser = SimpleMarkdown__default["default"].parserFor({
|
|
31445
|
+
math: {
|
|
31446
|
+
...pureMarkdown.pureMarkdownRules.math,
|
|
31447
|
+
order: 0
|
|
31448
|
+
},
|
|
31449
|
+
text: {
|
|
31450
|
+
order: 1,
|
|
31451
|
+
match: SimpleMarkdown__default["default"].anyScopeRegex(/^([^$\\{}]+)/),
|
|
31452
|
+
parse: capture => ({
|
|
31453
|
+
content: capture[0]
|
|
31454
|
+
})
|
|
31455
|
+
},
|
|
31456
|
+
specialCharacter: {
|
|
31457
|
+
order: 2,
|
|
31458
|
+
match: SimpleMarkdown__default["default"].anyScopeRegex(/^(\\[\S\s]|\$|\\$|{|})/),
|
|
31459
|
+
parse: capture => ({
|
|
31460
|
+
content: capture[0]
|
|
31461
|
+
})
|
|
31462
|
+
}
|
|
31463
|
+
}, {
|
|
31464
|
+
inline: true
|
|
31465
|
+
});
|
|
31466
|
+
|
|
31782
31467
|
/**
|
|
31783
31468
|
* Replace all text outside of the $ TeX blocks with `\\text{...}`
|
|
31784
31469
|
* This way, the entire resulting string can be rendered within <TeX>
|
|
@@ -31787,57 +31472,19 @@ function isUnlimitedGraphState(state) {
|
|
|
31787
31472
|
function replaceOutsideTeX(mathString) {
|
|
31788
31473
|
// All the information we need is in the first section,
|
|
31789
31474
|
// whether it's typed as "blockmath" or "paragraph"
|
|
31790
|
-
const
|
|
31791
|
-
|
|
31792
|
-
// If it's blockMath, the outer level has the full math content.
|
|
31793
|
-
if (firstSection.type === "blockMath") {
|
|
31794
|
-
return firstSection.content;
|
|
31795
|
-
}
|
|
31796
|
-
|
|
31797
|
-
// If it's a paragraph, we need to iterate through the sections
|
|
31798
|
-
// to look for individual math blocks.
|
|
31799
|
-
const condensedNodes = condenseTextNodes(firstSection.content);
|
|
31475
|
+
const parsed = mathOnlyParser(mathString);
|
|
31800
31476
|
let result = "";
|
|
31801
|
-
for (const piece of
|
|
31802
|
-
piece.type === "math" ? result += piece.content : result +=
|
|
31477
|
+
for (const piece of parsed) {
|
|
31478
|
+
piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
|
|
31803
31479
|
}
|
|
31804
|
-
return result
|
|
31480
|
+
return `\\text{${result}}`;
|
|
31805
31481
|
}
|
|
31806
|
-
|
|
31807
|
-
|
|
31808
|
-
|
|
31809
|
-
|
|
31810
|
-
|
|
31811
|
-
return result;
|
|
31812
|
-
}
|
|
31813
|
-
let currentText = "";
|
|
31814
|
-
for (const node of nodes) {
|
|
31815
|
-
if (node.type === "math") {
|
|
31816
|
-
if (currentText) {
|
|
31817
|
-
result.push({
|
|
31818
|
-
type: "text",
|
|
31819
|
-
content: currentText
|
|
31820
|
-
});
|
|
31821
|
-
currentText = "";
|
|
31822
|
-
}
|
|
31823
|
-
result.push(node);
|
|
31824
|
-
} else {
|
|
31825
|
-
currentText += node.content;
|
|
31826
|
-
}
|
|
31827
|
-
}
|
|
31828
|
-
if (currentText) {
|
|
31829
|
-
result.push({
|
|
31830
|
-
type: "text",
|
|
31831
|
-
content: currentText
|
|
31832
|
-
});
|
|
31482
|
+
function escapeIfUnescaped(character) {
|
|
31483
|
+
if (character.length === 1) {
|
|
31484
|
+
return "\\" + character;
|
|
31485
|
+
} else {
|
|
31486
|
+
return character;
|
|
31833
31487
|
}
|
|
31834
|
-
return result;
|
|
31835
|
-
}
|
|
31836
|
-
|
|
31837
|
-
// Helper function for replaceOutsideTeX()
|
|
31838
|
-
function escapeSpecialChars(str) {
|
|
31839
|
-
// Escape $, \, {, and } characters
|
|
31840
|
-
return str.replace(/([$\\{}])/g, "\\$1");
|
|
31841
31488
|
}
|
|
31842
31489
|
|
|
31843
31490
|
function AxisLabels() {
|
|
@@ -32786,12 +32433,17 @@ const ItemExtras = [
|
|
|
32786
32433
|
// prettier-ignore
|
|
32787
32434
|
|
|
32788
32435
|
// prettier-ignore
|
|
32789
|
-
|
|
32436
|
+
|
|
32790
32437
|
// prettier-ignore
|
|
32438
|
+
|
|
32791
32439
|
// prettier-ignore
|
|
32440
|
+
|
|
32792
32441
|
// prettier-ignore
|
|
32442
|
+
|
|
32793
32443
|
//prettier-ignore
|
|
32444
|
+
|
|
32794
32445
|
// A background image applied to various widgets.
|
|
32446
|
+
|
|
32795
32447
|
const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
|
|
32796
32448
|
|
|
32797
32449
|
// 2D range: xMin, xMax, yMin, yMax
|
|
@@ -33293,7 +32945,9 @@ const actions = {
|
|
|
33293
32945
|
removePoint,
|
|
33294
32946
|
focusPoint,
|
|
33295
32947
|
blurPoint,
|
|
33296
|
-
clickPoint
|
|
32948
|
+
clickPoint,
|
|
32949
|
+
closePolygon,
|
|
32950
|
+
openPolygon
|
|
33297
32951
|
},
|
|
33298
32952
|
quadratic: {
|
|
33299
32953
|
movePoint
|
|
@@ -33372,6 +33026,18 @@ function changeKeyboardInvitationVisibility(shouldShow) {
|
|
|
33372
33026
|
shouldShow
|
|
33373
33027
|
};
|
|
33374
33028
|
}
|
|
33029
|
+
const CLOSE_POLYGON = "close-polygon";
|
|
33030
|
+
function closePolygon() {
|
|
33031
|
+
return {
|
|
33032
|
+
type: CLOSE_POLYGON
|
|
33033
|
+
};
|
|
33034
|
+
}
|
|
33035
|
+
const OPEN_POLYGON = "open-polygon";
|
|
33036
|
+
function openPolygon() {
|
|
33037
|
+
return {
|
|
33038
|
+
type: OPEN_POLYGON
|
|
33039
|
+
};
|
|
33040
|
+
}
|
|
33375
33041
|
const MOVE_ALL = "move-all";
|
|
33376
33042
|
function moveAll(delta) {
|
|
33377
33043
|
return {
|
|
@@ -34915,33 +34581,17 @@ const LimitedPolygonGraph = props => {
|
|
|
34915
34581
|
}
|
|
34916
34582
|
})));
|
|
34917
34583
|
};
|
|
34918
|
-
|
|
34919
|
-
// TODO(catjohnson): reduce redundancy between LimitedPolygonGraph and UnlimitedPolygonGraph
|
|
34920
|
-
// both components are vary similar, however more implementation is needed to be added before
|
|
34921
|
-
// it is clear what can and can't be shared between components.
|
|
34922
34584
|
const UnlimitedPolygonGraph = props => {
|
|
34923
|
-
const [hovered, setHovered] = React__namespace.useState(false);
|
|
34924
|
-
// This is more so required for the re-rendering that occurs when state
|
|
34925
|
-
// updates; specifically with regard to line weighting and polygon focus.
|
|
34926
|
-
const [focusVisible, setFocusVisible] = React__namespace.useState(false);
|
|
34927
34585
|
const {
|
|
34928
34586
|
dispatch
|
|
34929
34587
|
} = props;
|
|
34930
34588
|
const {
|
|
34931
34589
|
coords,
|
|
34932
|
-
|
|
34933
|
-
showSides,
|
|
34934
|
-
range,
|
|
34935
|
-
snapStep,
|
|
34936
|
-
snapTo = "grid"
|
|
34590
|
+
closedPolygon
|
|
34937
34591
|
} = props.graphState;
|
|
34938
34592
|
const graphConfig = useGraphConfig();
|
|
34939
|
-
|
|
34940
|
-
// TODO(catjohnson): Explore abstracting this code as it is similar to point.tsx
|
|
34941
|
-
// and hopefully we can cut down ont the unlimited graph redundancy.
|
|
34942
34593
|
const {
|
|
34943
34594
|
range: [x, y],
|
|
34944
|
-
disableKeyboardInteraction,
|
|
34945
34595
|
graphDimensionsInPixels
|
|
34946
34596
|
} = graphConfig;
|
|
34947
34597
|
const widthPx = graphDimensionsInPixels[0];
|
|
@@ -34952,116 +34602,66 @@ const UnlimitedPolygonGraph = props => {
|
|
|
34952
34602
|
// TODO(benchristel): can the default set of points be removed here? I don't
|
|
34953
34603
|
// think coords can be null.
|
|
34954
34604
|
const points = coords ?? [[0, 0]];
|
|
34955
|
-
const polygonRef = React__namespace.useRef(null);
|
|
34956
|
-
const dragReferencePoint = points[0];
|
|
34957
|
-
const constrain = ["angles", "sides"].includes(snapTo) ? p => p : p => snap(snapStep, p);
|
|
34958
|
-
const {
|
|
34959
|
-
dragging
|
|
34960
|
-
} = useDraggable({
|
|
34961
|
-
gestureTarget: polygonRef,
|
|
34962
|
-
point: dragReferencePoint,
|
|
34963
|
-
onMove: newPoint => {
|
|
34964
|
-
const delta = mafs.vec.sub(newPoint, dragReferencePoint);
|
|
34965
|
-
dispatch(actions.polygon.moveAll(delta));
|
|
34966
|
-
},
|
|
34967
|
-
constrainKeyboardMovement: constrain
|
|
34968
|
-
});
|
|
34969
|
-
const lines = getLines(points);
|
|
34970
34605
|
React__namespace.useEffect(() => {
|
|
34971
34606
|
const focusedIndex = props.graphState.focusedPointIndex;
|
|
34972
34607
|
if (focusedIndex != null) {
|
|
34973
34608
|
pointRef.current[focusedIndex]?.focus();
|
|
34974
34609
|
}
|
|
34975
34610
|
}, [props.graphState.focusedPointIndex, pointRef]);
|
|
34976
|
-
|
|
34977
|
-
|
|
34978
|
-
|
|
34979
|
-
|
|
34980
|
-
}
|
|
34981
|
-
|
|
34982
|
-
|
|
34983
|
-
|
|
34984
|
-
y: top,
|
|
34985
|
-
onClick: event => {
|
|
34986
|
-
const elementRect = event.currentTarget.getBoundingClientRect();
|
|
34987
|
-
const x = event.clientX - elementRect.x;
|
|
34988
|
-
const y = event.clientY - elementRect.y;
|
|
34989
|
-
const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
|
|
34990
|
-
dispatch(actions.polygon.addPoint(graphCoordinates[0]));
|
|
34991
|
-
}
|
|
34992
|
-
}), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
|
|
34993
|
-
points: [...points],
|
|
34994
|
-
color: "var(--movable-line-stroke-color)",
|
|
34995
|
-
svgPolygonProps: {
|
|
34996
|
-
strokeWidth: focusVisible ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
|
|
34611
|
+
if (closedPolygon) {
|
|
34612
|
+
const closedPolygonProps = {
|
|
34613
|
+
...props,
|
|
34614
|
+
numSides: coords.length
|
|
34615
|
+
};
|
|
34616
|
+
return /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, closedPolygonProps);
|
|
34617
|
+
} else {
|
|
34618
|
+
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
|
|
34997
34619
|
style: {
|
|
34998
|
-
fill: "
|
|
34620
|
+
fill: "rgba(0,0,0,0)",
|
|
34621
|
+
cursor: "crosshair"
|
|
34622
|
+
},
|
|
34623
|
+
width: widthPx,
|
|
34624
|
+
height: heightPx,
|
|
34625
|
+
x: left,
|
|
34626
|
+
y: top,
|
|
34627
|
+
onClick: event => {
|
|
34628
|
+
const elementRect = event.currentTarget.getBoundingClientRect();
|
|
34629
|
+
const x = event.clientX - elementRect.x;
|
|
34630
|
+
const y = event.clientY - elementRect.y;
|
|
34631
|
+
const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
|
|
34632
|
+
dispatch(actions.polygon.addPoint(graphCoordinates[0]));
|
|
34633
|
+
}
|
|
34634
|
+
}), /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
|
|
34635
|
+
points: [...points],
|
|
34636
|
+
color: "var(--movable-line-stroke-color)",
|
|
34637
|
+
svgPolylineProps: {
|
|
34638
|
+
strokeWidth: "var(--movable-line-stroke-weight)",
|
|
34639
|
+
style: {
|
|
34640
|
+
fill: "transparent"
|
|
34641
|
+
}
|
|
34999
34642
|
}
|
|
35000
|
-
}
|
|
35001
|
-
|
|
35002
|
-
|
|
35003
|
-
|
|
35004
|
-
|
|
35005
|
-
|
|
35006
|
-
|
|
35007
|
-
return /*#__PURE__*/React__namespace.createElement(PolygonAngle, {
|
|
35008
|
-
key: "angle-" + i,
|
|
35009
|
-
centerPoint: point,
|
|
35010
|
-
endPoints: [pt1, pt2],
|
|
35011
|
-
range: range,
|
|
35012
|
-
polygonLines: lines,
|
|
35013
|
-
showAngles: !!showAngles,
|
|
35014
|
-
snapTo: snapTo
|
|
35015
|
-
});
|
|
35016
|
-
}), showSides && lines.map((_ref2, i) => {
|
|
35017
|
-
let [start, end] = _ref2;
|
|
35018
|
-
const [x, y] = mafs.vec.midpoint(start, end);
|
|
35019
|
-
const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
|
|
35020
|
-
return /*#__PURE__*/React__namespace.createElement(TextLabel, {
|
|
35021
|
-
key: "side-" + i,
|
|
35022
|
-
x: x,
|
|
35023
|
-
y: y
|
|
35024
|
-
}, !Number.isInteger(length) && "≈ ", length);
|
|
35025
|
-
}), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
|
|
35026
|
-
points: [...points],
|
|
35027
|
-
color: "transparent",
|
|
35028
|
-
svgPolygonProps: {
|
|
35029
|
-
ref: polygonRef,
|
|
35030
|
-
tabIndex: disableKeyboardInteraction ? -1 : 0,
|
|
35031
|
-
strokeWidth: TARGET_SIZE,
|
|
35032
|
-
style: {
|
|
35033
|
-
cursor: dragging ? "grabbing" : "grab",
|
|
35034
|
-
fill: hovered ? "var(--mafs-blue)" : "transparent"
|
|
34643
|
+
}), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34644
|
+
key: i,
|
|
34645
|
+
point: point,
|
|
34646
|
+
sequenceNumber: i + 1,
|
|
34647
|
+
onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
|
|
34648
|
+
ref: ref => {
|
|
34649
|
+
pointRef.current[i] = ref;
|
|
35035
34650
|
},
|
|
35036
|
-
|
|
35037
|
-
|
|
35038
|
-
// Required to remove line weighting when user clicks away
|
|
35039
|
-
// from the focused polygon
|
|
35040
|
-
onKeyDownCapture: () => {
|
|
35041
|
-
setFocusVisible(hasFocusVisible(polygonRef.current));
|
|
34651
|
+
onFocus: () => {
|
|
34652
|
+
dispatch(actions.polygon.focusPoint(i));
|
|
35042
34653
|
},
|
|
35043
|
-
|
|
35044
|
-
|
|
35045
|
-
|
|
35046
|
-
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
|
|
35051
|
-
|
|
35052
|
-
|
|
35053
|
-
|
|
35054
|
-
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
|
|
35055
|
-
ref: ref => {
|
|
35056
|
-
pointRef.current[i] = ref;
|
|
35057
|
-
},
|
|
35058
|
-
onFocus: () => {
|
|
35059
|
-
dispatch(actions.pointGraph.focusPoint(i));
|
|
35060
|
-
},
|
|
35061
|
-
onClick: () => {
|
|
35062
|
-
dispatch(actions.pointGraph.clickPoint(i));
|
|
35063
|
-
}
|
|
35064
|
-
})));
|
|
34654
|
+
onClick: () => {
|
|
34655
|
+
// If the point being clicked is the first point and
|
|
34656
|
+
// there's enough points to form a polygon (3 or more)
|
|
34657
|
+
// Close the shape before setting focus.
|
|
34658
|
+
if (i === 0 && props.graphState.coords.length >= 3) {
|
|
34659
|
+
dispatch(actions.polygon.closePolygon());
|
|
34660
|
+
}
|
|
34661
|
+
dispatch(actions.polygon.clickPoint(i));
|
|
34662
|
+
}
|
|
34663
|
+
})));
|
|
34664
|
+
}
|
|
35065
34665
|
};
|
|
35066
34666
|
function getLines(points) {
|
|
35067
34667
|
return points.map((point, i) => {
|
|
@@ -35457,6 +35057,7 @@ const MafsGraph = props => {
|
|
|
35457
35057
|
const uniqueId = React__namespace.useId();
|
|
35458
35058
|
const descriptionId = `interactive-graph-description-${uniqueId}`;
|
|
35459
35059
|
const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
|
|
35060
|
+
const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
|
|
35460
35061
|
const graphRef = React__namespace.useRef(null);
|
|
35461
35062
|
const {
|
|
35462
35063
|
analytics
|
|
@@ -35529,7 +35130,7 @@ const MafsGraph = props => {
|
|
|
35529
35130
|
handleKeyboardEvent(event, state, dispatch);
|
|
35530
35131
|
},
|
|
35531
35132
|
"aria-label": fullGraphAriaLabel,
|
|
35532
|
-
"aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
|
|
35133
|
+
"aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
|
|
35533
35134
|
ref: graphRef,
|
|
35534
35135
|
tabIndex: 0,
|
|
35535
35136
|
onFocus: event => {
|
|
@@ -35599,6 +35200,7 @@ const MafsGraph = props => {
|
|
|
35599
35200
|
height: height
|
|
35600
35201
|
}, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
35601
35202
|
style: {
|
|
35203
|
+
display: interactionPrompt ? undefined : "hidden",
|
|
35602
35204
|
textAlign: "center",
|
|
35603
35205
|
backgroundColor: "white",
|
|
35604
35206
|
border: "1px solid #21242C52",
|
|
@@ -35610,7 +35212,9 @@ const MafsGraph = props => {
|
|
|
35610
35212
|
top: "50%",
|
|
35611
35213
|
transform: "translateY(-50%)"
|
|
35612
35214
|
}
|
|
35613
|
-
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium,
|
|
35215
|
+
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
|
|
35216
|
+
id: unlimitedGraphKeyboardPromptId
|
|
35217
|
+
}, strings.graphKeyboardPrompt))), renderGraphControls({
|
|
35614
35218
|
state,
|
|
35615
35219
|
dispatch,
|
|
35616
35220
|
width,
|
|
@@ -35660,23 +35264,51 @@ const renderPointGraphControls = props => {
|
|
|
35660
35264
|
}
|
|
35661
35265
|
}, perseusStrings.removePoint));
|
|
35662
35266
|
};
|
|
35663
|
-
|
|
35664
|
-
/**
|
|
35665
|
-
* TODO(catjohnson): The polygon controls will be vastly different from the point controls.
|
|
35666
|
-
* Will be keeping this function as is for the time being, even if it's a copy of the point graph
|
|
35667
|
-
* controls.
|
|
35668
|
-
*/
|
|
35669
35267
|
const renderPolygonGraphControls = props => {
|
|
35670
35268
|
const {
|
|
35671
35269
|
interactionMode,
|
|
35672
35270
|
showRemovePointButton,
|
|
35673
|
-
focusedPointIndex
|
|
35271
|
+
focusedPointIndex,
|
|
35272
|
+
closedPolygon,
|
|
35273
|
+
coords
|
|
35674
35274
|
} = props.state;
|
|
35675
35275
|
const {
|
|
35676
35276
|
perseusStrings
|
|
35677
35277
|
} = props;
|
|
35678
35278
|
const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
|
|
35679
|
-
|
|
35279
|
+
|
|
35280
|
+
// We want to disable the closePolygon button when
|
|
35281
|
+
// there are not enough coords to make a polygon
|
|
35282
|
+
const disableCloseButton = coords.length < 3;
|
|
35283
|
+
|
|
35284
|
+
// If polygon is closed, show open button.
|
|
35285
|
+
// If polygon is open, show close button.
|
|
35286
|
+
const polygonButton = closedPolygon ? /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
|
|
35287
|
+
kind: "secondary",
|
|
35288
|
+
style: {
|
|
35289
|
+
width: "100%",
|
|
35290
|
+
marginLeft: "20px"
|
|
35291
|
+
},
|
|
35292
|
+
tabIndex: 0,
|
|
35293
|
+
onClick: () => {
|
|
35294
|
+
props.dispatch(actions.polygon.openPolygon());
|
|
35295
|
+
}
|
|
35296
|
+
}, perseusStrings.openPolygon) : /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
|
|
35297
|
+
kind: "secondary"
|
|
35298
|
+
// Conditional disable when there are less than 3 points in
|
|
35299
|
+
// the graph
|
|
35300
|
+
,
|
|
35301
|
+
disabled: disableCloseButton,
|
|
35302
|
+
style: {
|
|
35303
|
+
width: "100%",
|
|
35304
|
+
marginLeft: "20px"
|
|
35305
|
+
},
|
|
35306
|
+
tabIndex: disableCloseButton ? -1 : 0,
|
|
35307
|
+
onClick: () => {
|
|
35308
|
+
props.dispatch(actions.polygon.closePolygon());
|
|
35309
|
+
}
|
|
35310
|
+
}, perseusStrings.closePolygon);
|
|
35311
|
+
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
35680
35312
|
style: {
|
|
35681
35313
|
flexDirection: "row",
|
|
35682
35314
|
width: props.width
|
|
@@ -35686,8 +35318,13 @@ const renderPolygonGraphControls = props => {
|
|
|
35686
35318
|
style: {
|
|
35687
35319
|
width: "100%",
|
|
35688
35320
|
marginLeft: "20px"
|
|
35689
|
-
}
|
|
35690
|
-
|
|
35321
|
+
}
|
|
35322
|
+
// Disable button when polygon is closed.
|
|
35323
|
+
,
|
|
35324
|
+
disabled: closedPolygon
|
|
35325
|
+
// Do not make the button tabbable when it is disabled.
|
|
35326
|
+
,
|
|
35327
|
+
tabIndex: closedPolygon ? -1 : 0,
|
|
35691
35328
|
onClick: () => {
|
|
35692
35329
|
props.dispatch(actions.polygon.addPoint([0, 0]));
|
|
35693
35330
|
}
|
|
@@ -35695,19 +35332,21 @@ const renderPolygonGraphControls = props => {
|
|
|
35695
35332
|
id: REMOVE_BUTTON_ID,
|
|
35696
35333
|
kind: "secondary",
|
|
35697
35334
|
color: "destructive"
|
|
35335
|
+
// Disable button when polygon is closed.
|
|
35336
|
+
,
|
|
35337
|
+
disabled: closedPolygon || !shouldShowRemoveButton
|
|
35698
35338
|
// This button is meant to be interacted with by the mouse only
|
|
35699
35339
|
// Never allow learners to tab to this button
|
|
35700
35340
|
,
|
|
35701
35341
|
tabIndex: -1,
|
|
35702
35342
|
style: {
|
|
35703
35343
|
width: "100%",
|
|
35704
|
-
marginLeft: "20px"
|
|
35705
|
-
visibility: shouldShowRemoveButton ? "visible" : "hidden"
|
|
35344
|
+
marginLeft: "20px"
|
|
35706
35345
|
},
|
|
35707
35346
|
onClick: _event => {
|
|
35708
35347
|
props.dispatch(actions.polygon.removePoint(props.state.focusedPointIndex));
|
|
35709
35348
|
}
|
|
35710
|
-
}, perseusStrings.removePoint));
|
|
35349
|
+
}, perseusStrings.removePoint), polygonButton));
|
|
35711
35350
|
};
|
|
35712
35351
|
const renderGraphControls = props => {
|
|
35713
35352
|
const {
|
|
@@ -35758,14 +35397,17 @@ function handleBlurEvent(_event, state, dispatch) {
|
|
|
35758
35397
|
}
|
|
35759
35398
|
function handleKeyboardEvent(event, state, dispatch) {
|
|
35760
35399
|
if (isUnlimitedGraphState(state)) {
|
|
35761
|
-
if (event.key === "Backspace") {
|
|
35400
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
35762
35401
|
// TODO(benchristel): Checking classList here is a hack to prevent
|
|
35763
35402
|
// points from being deleted if the user presses the backspace key
|
|
35764
35403
|
// while the whole graph is focused. Instead of doing this, we
|
|
35765
35404
|
// should move the keyboard event handler to the movable point
|
|
35766
35405
|
// handle element.
|
|
35767
35406
|
if (document.activeElement?.classList.contains("movable-point__focusable-handle")) {
|
|
35768
|
-
|
|
35407
|
+
// Only allow delete if type is point or a polygon that is open.
|
|
35408
|
+
if (state.type === "point" || state.type === "polygon" && !state.closedPolygon) {
|
|
35409
|
+
dispatch(actions.global.deleteIntent());
|
|
35410
|
+
}
|
|
35769
35411
|
}
|
|
35770
35412
|
|
|
35771
35413
|
// After removing a point blur
|
|
@@ -36015,7 +35657,8 @@ function initializeGraphState(params) {
|
|
|
36015
35657
|
focusedPointIndex: null,
|
|
36016
35658
|
showRemovePointButton: false,
|
|
36017
35659
|
interactionMode: "mouse",
|
|
36018
|
-
showKeyboardInteractionInvitation: false
|
|
35660
|
+
showKeyboardInteractionInvitation: false,
|
|
35661
|
+
closedPolygon: false
|
|
36019
35662
|
};
|
|
36020
35663
|
case "point":
|
|
36021
35664
|
return {
|
|
@@ -36299,6 +35942,10 @@ function interactiveGraphReducer(state, action) {
|
|
|
36299
35942
|
return doDeleteIntent(state);
|
|
36300
35943
|
case CLICK_POINT:
|
|
36301
35944
|
return doClickPoint(state, action);
|
|
35945
|
+
case CLOSE_POLYGON:
|
|
35946
|
+
return doClosePolygon(state);
|
|
35947
|
+
case OPEN_POLYGON:
|
|
35948
|
+
return doOpenPolygon(state);
|
|
36302
35949
|
case CHANGE_INTERACTION_MODE:
|
|
36303
35950
|
return doChangeInteractionMode(state, action);
|
|
36304
35951
|
case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
|
|
@@ -36355,6 +36002,24 @@ function doClickPoint(state, action) {
|
|
|
36355
36002
|
}
|
|
36356
36003
|
return state;
|
|
36357
36004
|
}
|
|
36005
|
+
function doClosePolygon(state) {
|
|
36006
|
+
if (isUnlimitedGraphState(state) && state.type === "polygon") {
|
|
36007
|
+
return {
|
|
36008
|
+
...state,
|
|
36009
|
+
closedPolygon: true
|
|
36010
|
+
};
|
|
36011
|
+
}
|
|
36012
|
+
return state;
|
|
36013
|
+
}
|
|
36014
|
+
function doOpenPolygon(state) {
|
|
36015
|
+
if (isUnlimitedGraphState(state) && state.type === "polygon") {
|
|
36016
|
+
return {
|
|
36017
|
+
...state,
|
|
36018
|
+
closedPolygon: false
|
|
36019
|
+
};
|
|
36020
|
+
}
|
|
36021
|
+
return state;
|
|
36022
|
+
}
|
|
36358
36023
|
function doChangeInteractionMode(state, action) {
|
|
36359
36024
|
if (isUnlimitedGraphState(state)) {
|
|
36360
36025
|
const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
|
|
@@ -36562,8 +36227,12 @@ function doMovePoint(state, action) {
|
|
|
36562
36227
|
newValue: newValue
|
|
36563
36228
|
});
|
|
36564
36229
|
|
|
36565
|
-
//
|
|
36566
|
-
if
|
|
36230
|
+
// Boolean value to track whether we can let the polygon sides interact.
|
|
36231
|
+
// They can interact if it's an unlimited polygon that is open.
|
|
36232
|
+
const polygonSidesCanIntersect = state.numSides === "unlimited" && !state.closedPolygon;
|
|
36233
|
+
|
|
36234
|
+
// Reject the move if it would cause the sides of the polygon to cross.
|
|
36235
|
+
if (!polygonSidesCanIntersect && polygonSidesIntersect(newCoords)) {
|
|
36567
36236
|
return state;
|
|
36568
36237
|
}
|
|
36569
36238
|
return {
|
|
@@ -36728,14 +36397,15 @@ function doAddPoint(state, action) {
|
|
|
36728
36397
|
return state;
|
|
36729
36398
|
}
|
|
36730
36399
|
}
|
|
36400
|
+
const newCoords = [...state.coords, snappedPoint];
|
|
36731
36401
|
|
|
36732
36402
|
// If there's no point in spot where we want the new point to go we add it there
|
|
36733
36403
|
return {
|
|
36734
36404
|
...state,
|
|
36735
36405
|
hasBeenInteractedWith: true,
|
|
36736
|
-
coords:
|
|
36406
|
+
coords: newCoords,
|
|
36737
36407
|
showRemovePointButton: false,
|
|
36738
|
-
focusedPointIndex:
|
|
36408
|
+
focusedPointIndex: newCoords.length - 1
|
|
36739
36409
|
};
|
|
36740
36410
|
}
|
|
36741
36411
|
function doRemovePoint(state, action) {
|
|
@@ -39031,6 +38701,8 @@ var InteractiveGraph$1 = {
|
|
|
39031
38701
|
displayName: "Interactive graph (Assessments only)",
|
|
39032
38702
|
widget: InteractiveGraph,
|
|
39033
38703
|
staticTransform: staticTransform$3,
|
|
38704
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
38705
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
|
|
39034
38706
|
scorer: scoreInteractiveGraph
|
|
39035
38707
|
};
|
|
39036
38708
|
|
|
@@ -40040,6 +39712,8 @@ var LabelImage$1 = {
|
|
|
40040
39712
|
widget: LabelImageWithDependencies,
|
|
40041
39713
|
accessible: true,
|
|
40042
39714
|
isLintable: true,
|
|
39715
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
39716
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
|
|
40043
39717
|
scorer: scoreLabelImage
|
|
40044
39718
|
};
|
|
40045
39719
|
|
|
@@ -40979,6 +40653,8 @@ var Matcher$1 = {
|
|
|
40979
40653
|
displayName: "Matcher (two column)",
|
|
40980
40654
|
widget: Matcher,
|
|
40981
40655
|
isLintable: true,
|
|
40656
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
40657
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
|
|
40982
40658
|
scorer: scoreMatcher
|
|
40983
40659
|
};
|
|
40984
40660
|
|
|
@@ -40995,7 +40671,35 @@ const getPromptJSON$8 = (renderProps, userInput) => {
|
|
|
40995
40671
|
};
|
|
40996
40672
|
};
|
|
40997
40673
|
|
|
40674
|
+
/**
|
|
40675
|
+
* Checks user input from the matrix widget to see if it is scorable.
|
|
40676
|
+
*
|
|
40677
|
+
* Note: The matrix widget cannot do much validation without the Scoring
|
|
40678
|
+
* Data because of its use of KhanAnswerTypes as a core part of scoring.
|
|
40679
|
+
*
|
|
40680
|
+
* @see `scoreMatrix()` for more details.
|
|
40681
|
+
*/
|
|
40682
|
+
function validateMatrix(userInput, validationData, strings) {
|
|
40683
|
+
const supplied = userInput.answers;
|
|
40684
|
+
const suppliedSize = getMatrixSize(supplied);
|
|
40685
|
+
for (let row = 0; row < suppliedSize[0]; row++) {
|
|
40686
|
+
for (let col = 0; col < suppliedSize[1]; col++) {
|
|
40687
|
+
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
40688
|
+
return {
|
|
40689
|
+
type: "invalid",
|
|
40690
|
+
message: strings.fillAllCells
|
|
40691
|
+
};
|
|
40692
|
+
}
|
|
40693
|
+
}
|
|
40694
|
+
}
|
|
40695
|
+
return null;
|
|
40696
|
+
}
|
|
40697
|
+
|
|
40998
40698
|
function scoreMatrix(userInput, rubric, strings) {
|
|
40699
|
+
const validationResult = validateMatrix(userInput, rubric, strings);
|
|
40700
|
+
if (validationResult != null) {
|
|
40701
|
+
return validationResult;
|
|
40702
|
+
}
|
|
40999
40703
|
const solution = rubric.answers;
|
|
41000
40704
|
const supplied = userInput.answers;
|
|
41001
40705
|
const solutionSize = getMatrixSize(solution);
|
|
@@ -41003,13 +40707,9 @@ function scoreMatrix(userInput, rubric, strings) {
|
|
|
41003
40707
|
const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
|
|
41004
40708
|
const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
|
|
41005
40709
|
let message = null;
|
|
41006
|
-
let hasEmptyCell = false;
|
|
41007
40710
|
let incorrect = false;
|
|
41008
40711
|
___default["default"](suppliedSize[0]).times(row => {
|
|
41009
40712
|
___default["default"](suppliedSize[1]).times(col => {
|
|
41010
|
-
if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
|
|
41011
|
-
hasEmptyCell = true;
|
|
41012
|
-
}
|
|
41013
40713
|
if (!incorrectSize) {
|
|
41014
40714
|
const validator = createValidator(
|
|
41015
40715
|
// @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
|
|
@@ -41027,12 +40727,6 @@ function scoreMatrix(userInput, rubric, strings) {
|
|
|
41027
40727
|
}
|
|
41028
40728
|
});
|
|
41029
40729
|
});
|
|
41030
|
-
if (hasEmptyCell) {
|
|
41031
|
-
return {
|
|
41032
|
-
type: "invalid",
|
|
41033
|
-
message: strings.fillAllCells
|
|
41034
|
-
};
|
|
41035
|
-
}
|
|
41036
40730
|
if (incorrectSize) {
|
|
41037
40731
|
return {
|
|
41038
40732
|
type: "points",
|
|
@@ -41460,6 +41154,8 @@ var Matrix$1 = {
|
|
|
41460
41154
|
transform: propTransform$1,
|
|
41461
41155
|
staticTransform: staticTransform$2,
|
|
41462
41156
|
isLintable: true,
|
|
41157
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
41158
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
|
|
41463
41159
|
scorer: scoreMatrix
|
|
41464
41160
|
};
|
|
41465
41161
|
|
|
@@ -43095,6 +42791,8 @@ var NumberLine$1 = {
|
|
|
43095
42791
|
widget: NumberLine,
|
|
43096
42792
|
transform: numberLineTransform,
|
|
43097
42793
|
staticTransform: staticTransform$1,
|
|
42794
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
42795
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
|
|
43098
42796
|
scorer: scoreNumberLine
|
|
43099
42797
|
};
|
|
43100
42798
|
|
|
@@ -43669,6 +43367,8 @@ var Orderer$1 = {
|
|
|
43669
43367
|
hidden: true,
|
|
43670
43368
|
widget: Orderer,
|
|
43671
43369
|
isLintable: true,
|
|
43370
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
43371
|
+
// @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
|
|
43672
43372
|
scorer: scoreOrderer
|
|
43673
43373
|
};
|
|
43674
43374
|
|
|
@@ -44761,6 +44461,8 @@ var Plotter$1 = {
|
|
|
44761
44461
|
hidden: true,
|
|
44762
44462
|
widget: Plotter,
|
|
44763
44463
|
staticTransform: staticTransform,
|
|
44464
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
44465
|
+
// @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
|
|
44764
44466
|
scorer: scorePlotter
|
|
44765
44467
|
};
|
|
44766
44468
|
|
|
@@ -44944,6 +44646,8 @@ var Sorter$1 = {
|
|
|
44944
44646
|
displayName: "Sorter",
|
|
44945
44647
|
widget: Sorter,
|
|
44946
44648
|
isLintable: true,
|
|
44649
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
44650
|
+
// @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
|
|
44947
44651
|
scorer: scoreSorter
|
|
44948
44652
|
};
|
|
44949
44653
|
|
|
@@ -45236,6 +44940,8 @@ var Table$1 = {
|
|
|
45236
44940
|
transform: propTransform,
|
|
45237
44941
|
hidden: true,
|
|
45238
44942
|
isLintable: true,
|
|
44943
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
44944
|
+
// @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
|
|
45239
44945
|
scorer: scoreTable
|
|
45240
44946
|
};
|
|
45241
44947
|
|
|
@@ -45421,7 +45127,7 @@ const init = function () {
|
|
|
45421
45127
|
|
|
45422
45128
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45423
45129
|
const libName = "@khanacademy/perseus";
|
|
45424
|
-
const libVersion = "
|
|
45130
|
+
const libVersion = "43.0.1";
|
|
45425
45131
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45426
45132
|
|
|
45427
45133
|
const apiVersion = {
|
|
@@ -46026,13 +45732,6 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
46026
45732
|
this.props.onRendered(true);
|
|
46027
45733
|
}
|
|
46028
45734
|
}
|
|
46029
|
-
if (this.props.score && this.props.score !== prevProps.score) {
|
|
46030
|
-
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
46031
|
-
this.setState({
|
|
46032
|
-
questionCompleted: this.props.score.correct,
|
|
46033
|
-
questionHighlightedWidgets: emptyQuestionAreaWidgets
|
|
46034
|
-
});
|
|
46035
|
-
}
|
|
46036
45735
|
}
|
|
46037
45736
|
componentWillUnmount() {
|
|
46038
45737
|
if (this.blurTimeoutID != null) {
|
|
@@ -46121,7 +45820,7 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
46121
45820
|
|
|
46122
45821
|
/**
|
|
46123
45822
|
* Accepts a question area widgetId, or an answer area widgetId of
|
|
46124
|
-
* the form "answer-
|
|
45823
|
+
* the form "answer-input-number 1", or the string "answer-area"
|
|
46125
45824
|
* for the whole answer area (if the answer area is a single widget).
|
|
46126
45825
|
*/
|
|
46127
45826
|
_setWidgetProps(widgetId, newProps, callback) {
|
|
@@ -46168,37 +45867,18 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
46168
45867
|
return this.questionRenderer.getPromptJSON();
|
|
46169
45868
|
}
|
|
46170
45869
|
|
|
46171
|
-
/**
|
|
46172
|
-
* Returns an array of the widget `.getUserInput()` results
|
|
46173
|
-
*
|
|
46174
|
-
* TODO: can we remove this? Seems to be just for backwards
|
|
46175
|
-
* compatibility with old Perseus Chrome logging
|
|
46176
|
-
* @deprecated use getUserInput
|
|
46177
|
-
*/
|
|
46178
|
-
getUserInputLegacy() {
|
|
46179
|
-
return this.questionRenderer.getUserInput();
|
|
46180
|
-
}
|
|
46181
|
-
|
|
46182
|
-
/**
|
|
46183
|
-
* Returns an object of the widget `.getUserInput()` results
|
|
46184
|
-
*/
|
|
46185
|
-
getUserInput() {
|
|
46186
|
-
return this.questionRenderer.getUserInputMap();
|
|
46187
|
-
}
|
|
46188
|
-
|
|
46189
45870
|
/**
|
|
46190
45871
|
* Grades the item.
|
|
46191
|
-
*
|
|
46192
|
-
* @deprecated use scorePerseusItem
|
|
46193
45872
|
*/
|
|
46194
45873
|
scoreInput() {
|
|
46195
|
-
const
|
|
46196
|
-
const
|
|
45874
|
+
const guessAndScore = this.questionRenderer.guessAndScore();
|
|
45875
|
+
const guess = guessAndScore[0];
|
|
45876
|
+
const score = guessAndScore[1];
|
|
46197
45877
|
|
|
46198
45878
|
// Continue to include an empty guess for the now defunct answer area.
|
|
46199
45879
|
// TODO(alex): Check whether we rely on the format here for
|
|
46200
45880
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
46201
|
-
const maxCompatGuess = [
|
|
45881
|
+
const maxCompatGuess = [guess, []];
|
|
46202
45882
|
const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
46203
45883
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
46204
45884
|
this.setState({
|
|
@@ -48060,6 +47740,138 @@ const registerAllWidgetsForTesting = () => {
|
|
|
48060
47740
|
replaceDeprecatedWidgets();
|
|
48061
47741
|
};
|
|
48062
47742
|
|
|
47743
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
47744
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
47745
|
+
|
|
47746
|
+
/**
|
|
47747
|
+
* Regex for widget placeholders in a string.
|
|
47748
|
+
*
|
|
47749
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
47750
|
+
* Second capture group is the widget type (ex. "radio)
|
|
47751
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
47752
|
+
*/
|
|
47753
|
+
function getWidgetRegex() {
|
|
47754
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
47755
|
+
}
|
|
47756
|
+
|
|
47757
|
+
/**
|
|
47758
|
+
* Add a widget placeholder using the widget ID.
|
|
47759
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
47760
|
+
*
|
|
47761
|
+
* @param {string} id
|
|
47762
|
+
* @returns {string}
|
|
47763
|
+
*/
|
|
47764
|
+
function addWidget(id) {
|
|
47765
|
+
return `[[☃ ${id}]]`;
|
|
47766
|
+
}
|
|
47767
|
+
|
|
47768
|
+
/**
|
|
47769
|
+
* Get a widget type by a widget's ID
|
|
47770
|
+
* (please don't derive type from ID)
|
|
47771
|
+
*
|
|
47772
|
+
* @param {string} widgetId the ID of the widget
|
|
47773
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47774
|
+
* @returns {string} the widget type (ie "radio")
|
|
47775
|
+
*/
|
|
47776
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
47777
|
+
const widget = widgetMap[WidgetId];
|
|
47778
|
+
return widget?.type ?? null;
|
|
47779
|
+
}
|
|
47780
|
+
|
|
47781
|
+
/**
|
|
47782
|
+
* Does the content have a specific type of widget?
|
|
47783
|
+
*
|
|
47784
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
47785
|
+
* @param {string} content the string to search through
|
|
47786
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47787
|
+
* @returns {boolean} if the content includes the widget type
|
|
47788
|
+
*/
|
|
47789
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
47790
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
47791
|
+
}
|
|
47792
|
+
|
|
47793
|
+
/**
|
|
47794
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
47795
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
47796
|
+
*
|
|
47797
|
+
* Content should contain Perseus widget placeholders,
|
|
47798
|
+
* which look like: '[[☃ radio 1]]'.
|
|
47799
|
+
*
|
|
47800
|
+
* @param {string} content
|
|
47801
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
47802
|
+
*/
|
|
47803
|
+
function getWidgetIdsFromContent(content) {
|
|
47804
|
+
const widgets = [];
|
|
47805
|
+
const localWidgetRegex = getWidgetRegex();
|
|
47806
|
+
let match = localWidgetRegex.exec(content);
|
|
47807
|
+
while (match !== null) {
|
|
47808
|
+
widgets.push(match[1]);
|
|
47809
|
+
match = localWidgetRegex.exec(content);
|
|
47810
|
+
}
|
|
47811
|
+
return widgets;
|
|
47812
|
+
}
|
|
47813
|
+
|
|
47814
|
+
/**
|
|
47815
|
+
* Get a list of widget IDs from content,
|
|
47816
|
+
* but only for specific widget types
|
|
47817
|
+
*
|
|
47818
|
+
* @param {string} type the type of widget (ie "radio")
|
|
47819
|
+
* @param {string} content the string to parse
|
|
47820
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47821
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
47822
|
+
*/
|
|
47823
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
47824
|
+
const rv = [];
|
|
47825
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
47826
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
47827
|
+
const widget = widgetMap[widgetId];
|
|
47828
|
+
if (widget?.type === type) {
|
|
47829
|
+
rv.push(widgetId);
|
|
47830
|
+
}
|
|
47831
|
+
});
|
|
47832
|
+
return rv;
|
|
47833
|
+
}
|
|
47834
|
+
|
|
47835
|
+
/**
|
|
47836
|
+
* Pull the widget map out of ItemData
|
|
47837
|
+
*
|
|
47838
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
47839
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
47840
|
+
*/
|
|
47841
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
47842
|
+
return itemData.question.widgets;
|
|
47843
|
+
}
|
|
47844
|
+
|
|
47845
|
+
/**
|
|
47846
|
+
* Get the widget information from a WidgetMap
|
|
47847
|
+
*
|
|
47848
|
+
* @param {string} widgetId the ID of the widget
|
|
47849
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
47850
|
+
*/
|
|
47851
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
47852
|
+
return widgetMap[widgetId] ?? null;
|
|
47853
|
+
}
|
|
47854
|
+
|
|
47855
|
+
/**
|
|
47856
|
+
* Get select widgets from a widget map.
|
|
47857
|
+
* Useful for multi-items that needs to split
|
|
47858
|
+
* one PerseusItem into several
|
|
47859
|
+
*
|
|
47860
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
47861
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
47862
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
47863
|
+
*/
|
|
47864
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
47865
|
+
const widgets = {};
|
|
47866
|
+
widgetIds.forEach(widgetId => {
|
|
47867
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
47868
|
+
if (widget) {
|
|
47869
|
+
widgets[widgetId] = widget;
|
|
47870
|
+
}
|
|
47871
|
+
});
|
|
47872
|
+
return widgets;
|
|
47873
|
+
}
|
|
47874
|
+
|
|
48063
47875
|
/**
|
|
48064
47876
|
* This function extracts the answers from the widgets.
|
|
48065
47877
|
*
|
|
@@ -50061,8 +49873,6 @@ exports.buildMapper = buildMapper;
|
|
|
50061
49873
|
exports.components = components;
|
|
50062
49874
|
exports.containerSizeClass = containerSizeClass;
|
|
50063
49875
|
exports.contentHasWidgetType = contentHasWidgetType;
|
|
50064
|
-
exports.conversionRequired = conversionRequired;
|
|
50065
|
-
exports.convertDeprecatedWidgets = convertDeprecatedWidgets;
|
|
50066
49876
|
exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
|
|
50067
49877
|
exports.displaySigFigs = displaySigFigs;
|
|
50068
49878
|
exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
|
|
@@ -50109,12 +49919,12 @@ exports.libVersion = libVersion;
|
|
|
50109
49919
|
exports.lockedFigureColors = lockedFigureColors;
|
|
50110
49920
|
exports.lockedFigureFillStyles = lockedFigureFillStyles;
|
|
50111
49921
|
exports.makeSafeUrl = makeSafeUrl;
|
|
49922
|
+
exports.mathOnlyParser = mathOnlyParser;
|
|
50112
49923
|
exports.mediaQueries = mediaQueries;
|
|
50113
49924
|
exports.parsePerseusItem = parsePerseusItem;
|
|
50114
49925
|
exports.plotterPlotTypes = plotterPlotTypes;
|
|
50115
49926
|
exports.preprocessTex = preprocessTex;
|
|
50116
49927
|
exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
|
|
50117
|
-
exports.scorePerseusItem = scorePerseusItem;
|
|
50118
49928
|
exports.shapes = shapes;
|
|
50119
49929
|
exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
|
|
50120
49930
|
exports.traverse = traverse;
|