@khanacademy/perseus 20.8.1 → 21.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/index.js +413 -295
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +426 -303
- package/dist/index.js.map +1 -1
- package/dist/util/extract-perseus-data.d.ts +6 -7
- package/dist/util/snowman-utils.d.ts +0 -72
- package/dist/util/test-utils.d.ts +37 -2
- package/dist/util/widget-enum-utils.d.ts +1 -3
- package/dist/widget-type-utils.d.ts +64 -0
- package/dist/widgets/interactive-graph.d.ts +1 -1
- package/dist/widgets/interactive-graphs/graphs/components/angle.d.ts +12 -2
- package/dist/widgets/interactive-graphs/graphs/components/css-transform-wrapper.d.ts +7 -0
- package/dist/widgets/interactive-graphs/graphs/components/text-label.d.ts +6 -0
- package/dist/widgets/interactive-graphs/graphs/utils.d.ts +2 -0
- package/dist/widgets/interactive-graphs/reducer/interactive-graph-state.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18890,7 +18890,7 @@ const isAccessible = widgetInfo => {
|
|
|
18890
18890
|
}
|
|
18891
18891
|
return !!accessible;
|
|
18892
18892
|
};
|
|
18893
|
-
const getAllWidgetTypes
|
|
18893
|
+
const getAllWidgetTypes = () => {
|
|
18894
18894
|
return ___default["default"].keys(widgets);
|
|
18895
18895
|
};
|
|
18896
18896
|
const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
|
|
@@ -19162,7 +19162,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19162
19162
|
getVersionVector: getVersionVector,
|
|
19163
19163
|
getPublicWidgets: getPublicWidgets,
|
|
19164
19164
|
isAccessible: isAccessible,
|
|
19165
|
-
getAllWidgetTypes: getAllWidgetTypes
|
|
19165
|
+
getAllWidgetTypes: getAllWidgetTypes,
|
|
19166
19166
|
upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
|
|
19167
19167
|
getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
|
|
19168
19168
|
traverseChildWidgets: traverseChildWidgets$3,
|
|
@@ -19560,6 +19560,9 @@ class Renderer extends React__namespace.Component {
|
|
|
19560
19560
|
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19561
19561
|
const newValues = {};
|
|
19562
19562
|
if (!widgetInfo.type) {
|
|
19563
|
+
// TODO: why does widget have no type?
|
|
19564
|
+
// We don't want to derive type from widget ID
|
|
19565
|
+
// see: LEMS-1845
|
|
19563
19566
|
newValues.type = widgetId.split(" ")[0];
|
|
19564
19567
|
}
|
|
19565
19568
|
if (!widgetInfo.alignment) {
|
|
@@ -28416,6 +28419,10 @@ const getRayIntersectionCoords = (collinearPoint, extendFrom, range) => {
|
|
|
28416
28419
|
return [xMax, yAtXMax];
|
|
28417
28420
|
}
|
|
28418
28421
|
};
|
|
28422
|
+
const getLines = points => points.map((point, i) => {
|
|
28423
|
+
const next = points[(i + 1) % points.length];
|
|
28424
|
+
return [point, next];
|
|
28425
|
+
});
|
|
28419
28426
|
|
|
28420
28427
|
const defaultStroke = "var(--movable-line-stroke-color)";
|
|
28421
28428
|
const MovableLine = props => {
|
|
@@ -28676,12 +28683,55 @@ const RayGraph = props => {
|
|
|
28676
28683
|
}));
|
|
28677
28684
|
};
|
|
28678
28685
|
|
|
28686
|
+
/**
|
|
28687
|
+
* Wrapper with Mafs transform styles for SVG elements.
|
|
28688
|
+
* Scales and translates the SVG element to match the Mafs view.
|
|
28689
|
+
* Use if you do not need/want to use the hook in `use-transform.ts`.
|
|
28690
|
+
*/
|
|
28691
|
+
const MafsCssTransformWrapper = _ref => {
|
|
28692
|
+
let {
|
|
28693
|
+
children
|
|
28694
|
+
} = _ref;
|
|
28695
|
+
return /*#__PURE__*/React__namespace.createElement("g", {
|
|
28696
|
+
style: {
|
|
28697
|
+
transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
|
|
28698
|
+
}
|
|
28699
|
+
}, children);
|
|
28700
|
+
};
|
|
28701
|
+
|
|
28702
|
+
const TextLabel = _ref => {
|
|
28703
|
+
let {
|
|
28704
|
+
children,
|
|
28705
|
+
...rest
|
|
28706
|
+
} = _ref;
|
|
28707
|
+
return /*#__PURE__*/React__default["default"].createElement(mafs.Text, _extends({
|
|
28708
|
+
size: 16,
|
|
28709
|
+
svgTextProps: {
|
|
28710
|
+
filter: "url(#background)",
|
|
28711
|
+
fontWeight: "bold"
|
|
28712
|
+
}
|
|
28713
|
+
}, rest), children);
|
|
28714
|
+
};
|
|
28715
|
+
const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs", null, /*#__PURE__*/React__default["default"].createElement("filter", {
|
|
28716
|
+
id: "background",
|
|
28717
|
+
x: "-5%",
|
|
28718
|
+
width: "110%",
|
|
28719
|
+
y: "0%",
|
|
28720
|
+
height: "100%"
|
|
28721
|
+
}, /*#__PURE__*/React__default["default"].createElement("feFlood", {
|
|
28722
|
+
floodColor: "#FFF",
|
|
28723
|
+
floodOpacity: "0.64"
|
|
28724
|
+
}), /*#__PURE__*/React__default["default"].createElement("feComposite", {
|
|
28725
|
+
operator: "over",
|
|
28726
|
+
in: "SourceGraphic"
|
|
28727
|
+
})));
|
|
28728
|
+
|
|
28679
28729
|
const Angle = _ref => {
|
|
28680
28730
|
let {
|
|
28681
28731
|
centerPoint,
|
|
28682
28732
|
endPoints,
|
|
28683
28733
|
range,
|
|
28684
|
-
|
|
28734
|
+
polygonLines,
|
|
28685
28735
|
showAngles
|
|
28686
28736
|
} = _ref;
|
|
28687
28737
|
const [centerX, centerY] = centerPoint;
|
|
@@ -28703,7 +28753,7 @@ const Angle = _ref => {
|
|
|
28703
28753
|
// Used to calculate the inside of the angle and to create square
|
|
28704
28754
|
const [x3, y3] = mafs.vec.add(centerPoint, mafs.vec.add(mafs.vec.sub([x1, y1], centerPoint), mafs.vec.sub([x2, y2], centerPoint)));
|
|
28705
28755
|
if (!showAngles) {
|
|
28706
|
-
return isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(
|
|
28756
|
+
return isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
|
|
28707
28757
|
start: [x1, y1],
|
|
28708
28758
|
vertex: [x2, y2],
|
|
28709
28759
|
end: [x3, y3]
|
|
@@ -28711,13 +28761,13 @@ const Angle = _ref => {
|
|
|
28711
28761
|
}
|
|
28712
28762
|
|
|
28713
28763
|
// Midpoint betwen ends of arc
|
|
28714
|
-
const
|
|
28715
|
-
const largeArcFlag =
|
|
28716
|
-
const sweepFlag =
|
|
28764
|
+
const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
|
|
28765
|
+
const largeArcFlag = isOutside ? 1 : 0;
|
|
28766
|
+
const sweepFlag = isOutside ? 1 : 0;
|
|
28717
28767
|
const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
|
|
28718
28768
|
let angleInDegrees = angle * (180 / Math.PI);
|
|
28719
28769
|
// If we have triggered "largArcFlag", the angle should be greater than 180
|
|
28720
|
-
if (
|
|
28770
|
+
if (isOutside) {
|
|
28721
28771
|
angleInDegrees = 360 - angleInDegrees;
|
|
28722
28772
|
}
|
|
28723
28773
|
const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
|
|
@@ -28734,92 +28784,63 @@ const Angle = _ref => {
|
|
|
28734
28784
|
}), /*#__PURE__*/React__namespace.createElement("feComposite", {
|
|
28735
28785
|
operator: "over",
|
|
28736
28786
|
in: "SourceGraphic"
|
|
28737
|
-
}))), !
|
|
28787
|
+
}))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
|
|
28738
28788
|
start: [x1, y1],
|
|
28739
28789
|
vertex: [x2, y2],
|
|
28740
28790
|
end: [x3, y3]
|
|
28741
|
-
}) : /*#__PURE__*/React__namespace.createElement("
|
|
28742
|
-
style: {
|
|
28743
|
-
transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
|
|
28744
|
-
}
|
|
28745
|
-
}, /*#__PURE__*/React__namespace.createElement("path", {
|
|
28791
|
+
}) : /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
|
|
28746
28792
|
d: arc,
|
|
28747
28793
|
strokeWidth: 0.02,
|
|
28748
28794
|
fill: "none"
|
|
28749
|
-
})), /*#__PURE__*/React__namespace.createElement(
|
|
28795
|
+
})), /*#__PURE__*/React__namespace.createElement(TextLabel, {
|
|
28750
28796
|
x: x3,
|
|
28751
28797
|
y: y3,
|
|
28752
|
-
size: 15,
|
|
28753
|
-
svgTextProps: {
|
|
28754
|
-
filter: "url(#background)",
|
|
28755
|
-
fontWeight: "bold"
|
|
28756
|
-
}
|
|
28757
|
-
// Shift position if text is too close to movable point
|
|
28758
|
-
,
|
|
28759
28798
|
attach: y3 - centerY > 0 ? "s" : "n",
|
|
28760
28799
|
attachDistance: Math.abs(y3 - centerY) < 0.2 || mafs.vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
|
|
28761
28800
|
}, angleLabel, "\xB0"));
|
|
28762
28801
|
};
|
|
28763
|
-
|
|
28802
|
+
|
|
28803
|
+
/**
|
|
28804
|
+
* This is broken out into its own component so that it can be used for an early return
|
|
28805
|
+
* (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
|
|
28806
|
+
*/
|
|
28807
|
+
const RightAngleSquare = _ref2 => {
|
|
28764
28808
|
let {
|
|
28765
28809
|
start: [x1, y1],
|
|
28766
28810
|
vertex: [x2, y2],
|
|
28767
28811
|
end: [x3, y3]
|
|
28768
28812
|
} = _ref2;
|
|
28769
|
-
return /*#__PURE__*/React__namespace.createElement("
|
|
28770
|
-
style: {
|
|
28771
|
-
transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
|
|
28772
|
-
}
|
|
28773
|
-
}, /*#__PURE__*/React__namespace.createElement("path", {
|
|
28813
|
+
return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
|
|
28774
28814
|
d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
|
|
28775
28815
|
strokeWidth: 0.02,
|
|
28776
28816
|
fill: "none"
|
|
28777
28817
|
}));
|
|
28778
28818
|
};
|
|
28779
28819
|
const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
|
|
28780
|
-
|
|
28781
|
-
|
|
28782
|
-
|
|
28783
|
-
|
|
28784
|
-
|
|
28785
|
-
|
|
28786
|
-
|
|
28787
|
-
|
|
28788
|
-
|
|
28789
|
-
|
|
28790
|
-
|
|
28791
|
-
|
|
28792
|
-
|
|
28793
|
-
|
|
28794
|
-
// get array sans point and adjacent points
|
|
28795
|
-
const [prev, next] = [(midpointIndex - 1 + polygonPoints.length) % polygonPoints.length, (midpointIndex + 1) % polygonPoints.length];
|
|
28796
|
-
const [[lineAx, lineAy], [lineBx, lineBy]] = [midPoint, rangeIntersectionPoint];
|
|
28797
|
-
for (let i = 0; i < polygonPoints.length; i++) {
|
|
28798
|
-
if ([prev, midpointIndex, next].includes(i)) {
|
|
28799
|
-
continue;
|
|
28800
|
-
}
|
|
28801
|
-
const [x, y] = polygonPoints[i];
|
|
28802
|
-
const intersectsPoint = (y - lineAy) * (lineBx - lineAx) === (lineBy - lineAy) * (x - lineAx);
|
|
28803
|
-
if (intersectsPoint) {
|
|
28804
|
-
return false;
|
|
28805
|
-
}
|
|
28806
|
-
}
|
|
28807
|
-
return true;
|
|
28808
|
-
}
|
|
28820
|
+
|
|
28821
|
+
/**
|
|
28822
|
+
* Determines if an angle is an inside (false) or outside (true) angle.
|
|
28823
|
+
* They way, we know to flip the `largeArc` and `sweepArc` flags.
|
|
28824
|
+
* Uses the priciple that a ray from a point inside a polygon will intersect
|
|
28825
|
+
* with an odd number of lines, while a ray from a point outside the polygon
|
|
28826
|
+
* will intersect with an even number of lines.
|
|
28827
|
+
* https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
|
|
28828
|
+
*/
|
|
28829
|
+
const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
|
|
28830
|
+
// Create a ray from the midpoint (inside angle) to the edge of the range
|
|
28831
|
+
const rangeIntersectionPoint = getRayIntersectionCoords(midpoint, vertex, range);
|
|
28832
|
+
let lineIntersectionCount = 0;
|
|
28833
|
+
polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
|
|
28809
28834
|
|
|
28810
28835
|
// If the number of intersections is even, the angle is inside the polygon
|
|
28811
|
-
return isEven(
|
|
28836
|
+
return !isEven(lineIntersectionCount);
|
|
28812
28837
|
};
|
|
28813
|
-
const getLines = points => points.map((point, i) => {
|
|
28814
|
-
const next = points[(i + 1) % points.length];
|
|
28815
|
-
return [point, next];
|
|
28816
|
-
});
|
|
28817
28838
|
|
|
28818
28839
|
// https://stackoverflow.com/a/24392281/7347484
|
|
28819
28840
|
// The "intersects" function in geometry doesn't seem to work for this use case
|
|
28820
|
-
const linesIntersect = (
|
|
28821
|
-
let [[a, b], [c, d]] =
|
|
28822
|
-
let [[p, q], [r, s]] =
|
|
28841
|
+
const linesIntersect = (_ref3, _ref4) => {
|
|
28842
|
+
let [[a, b], [c, d]] = _ref3;
|
|
28843
|
+
let [[p, q], [r, s]] = _ref4;
|
|
28823
28844
|
const determinant = (c - a) * (s - q) - (r - p) * (d - b);
|
|
28824
28845
|
if (determinant === 0) {
|
|
28825
28846
|
return false;
|
|
@@ -28841,6 +28862,7 @@ const PolygonGraph = props => {
|
|
|
28841
28862
|
coords,
|
|
28842
28863
|
type,
|
|
28843
28864
|
showAngles,
|
|
28865
|
+
showSides,
|
|
28844
28866
|
range
|
|
28845
28867
|
} = props.graphState;
|
|
28846
28868
|
const points = coords ?? [[0, 0]];
|
|
@@ -28859,6 +28881,7 @@ const PolygonGraph = props => {
|
|
|
28859
28881
|
constrain: p => p
|
|
28860
28882
|
});
|
|
28861
28883
|
const active = hovered || focused || dragging;
|
|
28884
|
+
const lines = getLines(points);
|
|
28862
28885
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
|
|
28863
28886
|
points: [...points],
|
|
28864
28887
|
color: "var(--movable-line-stroke-color)",
|
|
@@ -28877,9 +28900,18 @@ const PolygonGraph = props => {
|
|
|
28877
28900
|
endPoints: [pt1, pt2],
|
|
28878
28901
|
active: active,
|
|
28879
28902
|
range: range,
|
|
28880
|
-
|
|
28903
|
+
polygonLines: lines,
|
|
28881
28904
|
showAngles: !!showAngles
|
|
28882
28905
|
});
|
|
28906
|
+
}), showSides && lines.map((_ref, i) => {
|
|
28907
|
+
let [start, end] = _ref;
|
|
28908
|
+
const [x, y] = mafs.vec.midpoint(start, end);
|
|
28909
|
+
const length = parseFloat(mafs.vec.dist(start, end).toFixed(1));
|
|
28910
|
+
return /*#__PURE__*/React__namespace.createElement(TextLabel, {
|
|
28911
|
+
key: "side-" + i,
|
|
28912
|
+
x: x,
|
|
28913
|
+
y: y
|
|
28914
|
+
}, !Number.isInteger(length) && "≈ ", length);
|
|
28883
28915
|
}), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
|
|
28884
28916
|
points: [...points],
|
|
28885
28917
|
color: "transparent",
|
|
@@ -29897,31 +29929,41 @@ function initializeGraphState(params) {
|
|
|
29897
29929
|
// Instead of updating to v5, using generic type to relate input and output types.
|
|
29898
29930
|
function getGradableGraph(state, initialGraph) {
|
|
29899
29931
|
if (!state.hasBeenInteractedWith) {
|
|
29900
|
-
return
|
|
29932
|
+
return {
|
|
29933
|
+
type: initialGraph.type
|
|
29934
|
+
};
|
|
29901
29935
|
}
|
|
29902
|
-
|
|
29903
|
-
|
|
29904
|
-
|
|
29905
|
-
|
|
29906
|
-
|
|
29907
|
-
|
|
29908
|
-
|
|
29909
|
-
|
|
29910
|
-
|
|
29911
|
-
|
|
29912
|
-
|
|
29913
|
-
|
|
29914
|
-
|
|
29915
|
-
|
|
29916
|
-
|
|
29917
|
-
|
|
29918
|
-
|
|
29919
|
-
|
|
29920
|
-
|
|
29921
|
-
|
|
29922
|
-
|
|
29923
|
-
|
|
29936
|
+
if (initialGraph.type === "linear-system" && state.type === "linear-system") {
|
|
29937
|
+
return {
|
|
29938
|
+
...initialGraph,
|
|
29939
|
+
coords: state.coords
|
|
29940
|
+
};
|
|
29941
|
+
}
|
|
29942
|
+
if (state.type === "segment" && initialGraph.type === "segment") {
|
|
29943
|
+
return {
|
|
29944
|
+
...initialGraph,
|
|
29945
|
+
coords: state.coords
|
|
29946
|
+
};
|
|
29947
|
+
}
|
|
29948
|
+
if (state.type === "linear" && initialGraph.type === "linear") {
|
|
29949
|
+
return {
|
|
29950
|
+
...initialGraph,
|
|
29951
|
+
coords: state.coords?.[0]
|
|
29952
|
+
};
|
|
29953
|
+
}
|
|
29954
|
+
if (state.type === "ray" && initialGraph.type === "ray") {
|
|
29955
|
+
return {
|
|
29956
|
+
...initialGraph,
|
|
29957
|
+
coords: state.coords?.[0]
|
|
29958
|
+
};
|
|
29959
|
+
}
|
|
29960
|
+
if (state.type === "polygon" && initialGraph.type === "polygon") {
|
|
29961
|
+
return {
|
|
29962
|
+
...initialGraph,
|
|
29963
|
+
coords: state.coords
|
|
29964
|
+
};
|
|
29924
29965
|
}
|
|
29966
|
+
throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
|
|
29925
29967
|
}
|
|
29926
29968
|
const getDefaultSegments = _ref => {
|
|
29927
29969
|
let {
|
|
@@ -30061,7 +30103,7 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
|
|
|
30061
30103
|
zoom: false,
|
|
30062
30104
|
width: width,
|
|
30063
30105
|
height: height
|
|
30064
|
-
}, /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
|
|
30106
|
+
}, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
|
|
30065
30107
|
lockedFigures: props.lockedFigures
|
|
30066
30108
|
}), renderGraph({
|
|
30067
30109
|
state,
|
|
@@ -31687,18 +31729,18 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31687
31729
|
const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
|
|
31688
31730
|
return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
|
|
31689
31731
|
}
|
|
31690
|
-
static validate(
|
|
31691
|
-
// TODO(kevinb): rename state to userInput
|
|
31692
|
-
// state === userInput
|
|
31693
|
-
state, rubric, component) {
|
|
31732
|
+
static validate(userInput, rubric, component) {
|
|
31694
31733
|
// When nothing has moved, there will neither be coords nor the
|
|
31695
31734
|
// circle's center/radius fields. When those fields are absent, skip
|
|
31696
31735
|
// all these checks; just go mark the answer as empty.
|
|
31697
|
-
|
|
31698
|
-
|
|
31699
|
-
|
|
31700
|
-
|
|
31701
|
-
|
|
31736
|
+
const hasValue = Boolean(
|
|
31737
|
+
// @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
|
|
31738
|
+
userInput.coords ||
|
|
31739
|
+
// @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
|
|
31740
|
+
userInput.center && userInput.radius);
|
|
31741
|
+
if (userInput.type === rubric.correct.type && hasValue) {
|
|
31742
|
+
if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
|
|
31743
|
+
const guess = userInput.coords;
|
|
31702
31744
|
const correct = rubric.correct.coords;
|
|
31703
31745
|
|
|
31704
31746
|
// If both of the guess points are on the correct line, it's
|
|
@@ -31715,8 +31757,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31715
31757
|
message: null
|
|
31716
31758
|
};
|
|
31717
31759
|
}
|
|
31718
|
-
} else if (
|
|
31719
|
-
const guess =
|
|
31760
|
+
} else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
|
|
31761
|
+
const guess = userInput.coords;
|
|
31720
31762
|
const correct = rubric.correct.coords;
|
|
31721
31763
|
if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
|
|
31722
31764
|
return {
|
|
@@ -31726,9 +31768,9 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31726
31768
|
message: null
|
|
31727
31769
|
};
|
|
31728
31770
|
}
|
|
31729
|
-
} else if (
|
|
31771
|
+
} else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
|
|
31730
31772
|
// If the parabola coefficients match, it's correct.
|
|
31731
|
-
const guessCoeffs = this.getQuadraticCoefficients(
|
|
31773
|
+
const guessCoeffs = this.getQuadraticCoefficients(userInput.coords);
|
|
31732
31774
|
const correctCoeffs = this.getQuadraticCoefficients(
|
|
31733
31775
|
// @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
|
|
31734
31776
|
rubric.correct.coords);
|
|
@@ -31740,8 +31782,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31740
31782
|
message: null
|
|
31741
31783
|
};
|
|
31742
31784
|
}
|
|
31743
|
-
} else if (
|
|
31744
|
-
const guessCoeffs = this.getSinusoidCoefficients(
|
|
31785
|
+
} else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
|
|
31786
|
+
const guessCoeffs = this.getSinusoidCoefficients(userInput.coords);
|
|
31745
31787
|
const correctCoeffs = this.getSinusoidCoefficients(
|
|
31746
31788
|
// @ts-expect-error - TS2345 - Argument of type 'readonly Coord[] | undefined' is not assignable to parameter of type 'readonly Coord[]'.
|
|
31747
31789
|
rubric.correct.coords);
|
|
@@ -31756,8 +31798,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31756
31798
|
message: null
|
|
31757
31799
|
};
|
|
31758
31800
|
}
|
|
31759
|
-
} else if (
|
|
31760
|
-
if (deepEq$1(
|
|
31801
|
+
} else if (userInput.type === "circle" && rubric.correct.type === "circle") {
|
|
31802
|
+
if (deepEq$1(userInput.center, rubric.correct.center) && eq(userInput.radius, rubric.correct.radius)) {
|
|
31761
31803
|
return {
|
|
31762
31804
|
type: "points",
|
|
31763
31805
|
earned: 1,
|
|
@@ -31765,9 +31807,9 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31765
31807
|
message: null
|
|
31766
31808
|
};
|
|
31767
31809
|
}
|
|
31768
|
-
} else if (
|
|
31810
|
+
} else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
|
|
31769
31811
|
let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
|
|
31770
|
-
const guess =
|
|
31812
|
+
const guess = userInput.coords?.slice();
|
|
31771
31813
|
correct = correct.slice();
|
|
31772
31814
|
// Everything's already rounded so we shouldn't need to do an
|
|
31773
31815
|
// eq() comparison but _.isEqual(0, -0) is false, so we'll use
|
|
@@ -31784,8 +31826,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31784
31826
|
message: null
|
|
31785
31827
|
};
|
|
31786
31828
|
}
|
|
31787
|
-
} else if (
|
|
31788
|
-
const guess =
|
|
31829
|
+
} else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
|
|
31830
|
+
const guess = userInput.coords?.slice();
|
|
31789
31831
|
// @ts-expect-error - TS2322 - Type 'Coord[] | undefined' is not assignable to type 'Coord[]'.
|
|
31790
31832
|
const correct = rubric.correct.coords?.slice();
|
|
31791
31833
|
let match;
|
|
@@ -31809,8 +31851,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31809
31851
|
message: null
|
|
31810
31852
|
};
|
|
31811
31853
|
}
|
|
31812
|
-
} else if (
|
|
31813
|
-
let guess =
|
|
31854
|
+
} else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
|
|
31855
|
+
let guess = userInput.coords.slice();
|
|
31814
31856
|
let correct = rubric.correct.coords?.slice();
|
|
31815
31857
|
guess = ___default["default"].invoke(guess, "sort").sort();
|
|
31816
31858
|
// @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
|
|
@@ -31823,8 +31865,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31823
31865
|
message: null
|
|
31824
31866
|
};
|
|
31825
31867
|
}
|
|
31826
|
-
} else if (
|
|
31827
|
-
const guess =
|
|
31868
|
+
} else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
|
|
31869
|
+
const guess = userInput.coords;
|
|
31828
31870
|
const correct = rubric.correct.coords;
|
|
31829
31871
|
if (
|
|
31830
31872
|
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
@@ -31838,8 +31880,8 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31838
31880
|
message: null
|
|
31839
31881
|
};
|
|
31840
31882
|
}
|
|
31841
|
-
} else if (
|
|
31842
|
-
const guess =
|
|
31883
|
+
} else if (userInput.type === "angle" && rubric.correct.type === "angle") {
|
|
31884
|
+
const guess = userInput.coords;
|
|
31843
31885
|
const correct = rubric.correct.coords;
|
|
31844
31886
|
let match;
|
|
31845
31887
|
if (rubric.correct.match === "congruent") {
|
|
@@ -31878,7 +31920,7 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
31878
31920
|
|
|
31879
31921
|
// The input wasn't correct, so check if it's a blank input or if it's
|
|
31880
31922
|
// actually just wrong
|
|
31881
|
-
if (!hasValue || ___default["default"].isEqual(
|
|
31923
|
+
if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
|
|
31882
31924
|
// We're where we started.
|
|
31883
31925
|
return {
|
|
31884
31926
|
type: "invalid",
|
|
@@ -42073,7 +42115,7 @@ const init = function (options) {
|
|
|
42073
42115
|
|
|
42074
42116
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
42075
42117
|
const libName = "@khanacademy/perseus";
|
|
42076
|
-
const libVersion = "
|
|
42118
|
+
const libVersion = "21.0.0";
|
|
42077
42119
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
42078
42120
|
|
|
42079
42121
|
const apiVersion = {
|
|
@@ -44322,6 +44364,138 @@ const registerAllWidgetsForTesting = () => {
|
|
|
44322
44364
|
replaceDeprecatedWidgets();
|
|
44323
44365
|
};
|
|
44324
44366
|
|
|
44367
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
44368
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
44369
|
+
|
|
44370
|
+
/**
|
|
44371
|
+
* Regex for widget placeholders in a string.
|
|
44372
|
+
*
|
|
44373
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
44374
|
+
* Second capture group is the widget type (ex. "radio)
|
|
44375
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
44376
|
+
*/
|
|
44377
|
+
function getWidgetRegex() {
|
|
44378
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
44379
|
+
}
|
|
44380
|
+
|
|
44381
|
+
/**
|
|
44382
|
+
* Add a widget placeholder using the widget ID.
|
|
44383
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
44384
|
+
*
|
|
44385
|
+
* @param {string} id
|
|
44386
|
+
* @returns {string}
|
|
44387
|
+
*/
|
|
44388
|
+
function addWidget(id) {
|
|
44389
|
+
return `[[☃ ${id}]]`;
|
|
44390
|
+
}
|
|
44391
|
+
|
|
44392
|
+
/**
|
|
44393
|
+
* Get a widget type by a widget's ID
|
|
44394
|
+
* (please don't derive type from ID)
|
|
44395
|
+
*
|
|
44396
|
+
* @param {string} widgetId the ID of the widget
|
|
44397
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
44398
|
+
* @returns {string} the widget type (ie "radio")
|
|
44399
|
+
*/
|
|
44400
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
44401
|
+
const widget = widgetMap[WidgetId];
|
|
44402
|
+
return widget?.type ?? null;
|
|
44403
|
+
}
|
|
44404
|
+
|
|
44405
|
+
/**
|
|
44406
|
+
* Does the content have a specific type of widget?
|
|
44407
|
+
*
|
|
44408
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
44409
|
+
* @param {string} content the string to search through
|
|
44410
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
44411
|
+
* @returns {boolean} if the content includes the widget type
|
|
44412
|
+
*/
|
|
44413
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
44414
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
44415
|
+
}
|
|
44416
|
+
|
|
44417
|
+
/**
|
|
44418
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
44419
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
44420
|
+
*
|
|
44421
|
+
* Content should contain Perseus widget placeholders,
|
|
44422
|
+
* which look like: '[[☃ radio 1]]'.
|
|
44423
|
+
*
|
|
44424
|
+
* @param {string} content
|
|
44425
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
44426
|
+
*/
|
|
44427
|
+
function getWidgetIdsFromContent(content) {
|
|
44428
|
+
const widgets = [];
|
|
44429
|
+
const localWidgetRegex = getWidgetRegex();
|
|
44430
|
+
let match = localWidgetRegex.exec(content);
|
|
44431
|
+
while (match !== null) {
|
|
44432
|
+
widgets.push(match[1]);
|
|
44433
|
+
match = localWidgetRegex.exec(content);
|
|
44434
|
+
}
|
|
44435
|
+
return widgets;
|
|
44436
|
+
}
|
|
44437
|
+
|
|
44438
|
+
/**
|
|
44439
|
+
* Get a list of widget IDs from content,
|
|
44440
|
+
* but only for specific widget types
|
|
44441
|
+
*
|
|
44442
|
+
* @param {string} type the type of widget (ie "radio")
|
|
44443
|
+
* @param {string} content the string to parse
|
|
44444
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
44445
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
44446
|
+
*/
|
|
44447
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
44448
|
+
const rv = [];
|
|
44449
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
44450
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
44451
|
+
const widget = widgetMap[widgetId];
|
|
44452
|
+
if (widget?.type === type) {
|
|
44453
|
+
rv.push(widgetId);
|
|
44454
|
+
}
|
|
44455
|
+
});
|
|
44456
|
+
return rv;
|
|
44457
|
+
}
|
|
44458
|
+
|
|
44459
|
+
/**
|
|
44460
|
+
* Pull the widget map out of ItemData
|
|
44461
|
+
*
|
|
44462
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
44463
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
44464
|
+
*/
|
|
44465
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
44466
|
+
return itemData.question.widgets;
|
|
44467
|
+
}
|
|
44468
|
+
|
|
44469
|
+
/**
|
|
44470
|
+
* Get the widget information from a WidgetMap
|
|
44471
|
+
*
|
|
44472
|
+
* @param {string} widgetId the ID of the widget
|
|
44473
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
44474
|
+
*/
|
|
44475
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
44476
|
+
return widgetMap[widgetId] ?? null;
|
|
44477
|
+
}
|
|
44478
|
+
|
|
44479
|
+
/**
|
|
44480
|
+
* Get select widgets from a widget map.
|
|
44481
|
+
* Useful for multi-items that needs to split
|
|
44482
|
+
* one PerseusItem into several
|
|
44483
|
+
*
|
|
44484
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
44485
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
44486
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
44487
|
+
*/
|
|
44488
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
44489
|
+
const widgets = {};
|
|
44490
|
+
widgetIds.forEach(widgetId => {
|
|
44491
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
44492
|
+
if (widget) {
|
|
44493
|
+
widgets[widgetId] = widget;
|
|
44494
|
+
}
|
|
44495
|
+
});
|
|
44496
|
+
return widgets;
|
|
44497
|
+
}
|
|
44498
|
+
|
|
44325
44499
|
/**
|
|
44326
44500
|
* This function extracts the answers from the widgets.
|
|
44327
44501
|
*
|
|
@@ -44726,18 +44900,15 @@ const INDIVIDUAL_ANSWER_WIDGETS = ["interactive-graph", "categorizer", "grapher"
|
|
|
44726
44900
|
|
|
44727
44901
|
/* Widgets that are supported for automatic scoring */
|
|
44728
44902
|
const SUPPORTED_WIDGETS = ["radio", "numeric-input", "input-number", "expression", ...INDIVIDUAL_ANSWER_WIDGETS];
|
|
44729
|
-
const getWidgetTypeFromWidgetKey = widgetKey => {
|
|
44730
|
-
return widgetKey.split(" ")[0];
|
|
44731
|
-
};
|
|
44732
44903
|
|
|
44733
44904
|
/* Verify if the perseus item has supported widgets for automatic scoring */
|
|
44734
|
-
const isWrongAnswerSupported =
|
|
44735
|
-
return
|
|
44905
|
+
const isWrongAnswerSupported = (widgetIds, widgetMap) => {
|
|
44906
|
+
return widgetIds.length !== 0 && widgetIds.every(widgetId => SUPPORTED_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap)));
|
|
44736
44907
|
};
|
|
44737
44908
|
|
|
44738
|
-
/* Verify if the widget
|
|
44739
|
-
const shouldHaveIndividualAnswer =
|
|
44740
|
-
return INDIVIDUAL_ANSWER_WIDGETS.includes(
|
|
44909
|
+
/* Verify if the widget ID has an individual answer for the coach report view */
|
|
44910
|
+
const shouldHaveIndividualAnswer = (widgetId, widgetMap) => {
|
|
44911
|
+
return INDIVIDUAL_ANSWER_WIDGETS.includes(getWidgetTypeByWidgetId(widgetId, widgetMap));
|
|
44741
44912
|
};
|
|
44742
44913
|
|
|
44743
44914
|
/* Returns the answer userInput submission for currently supported widgets */
|
|
@@ -44756,29 +44927,30 @@ const getAnswerFromUserInput = (widgetType, userInput) => {
|
|
|
44756
44927
|
return userInput;
|
|
44757
44928
|
};
|
|
44758
44929
|
|
|
44759
|
-
/* Returns the correct answer for a given widget
|
|
44930
|
+
/* Returns the correct answer for a given widget ID and Perseus Item */
|
|
44760
44931
|
// TODO (LEMS-1835): We should fix the resonse type from getWidget to be specific.
|
|
44761
44932
|
// TODO (LEMS-1836): We should also consider adding the getOneCorrectAnswerFromRubric method to all widgets.
|
|
44762
|
-
const
|
|
44763
|
-
const rubric = itemData.question.widgets[
|
|
44764
|
-
const
|
|
44933
|
+
const getCorrectAnswerForWidgetId = (widgetId, itemData) => {
|
|
44934
|
+
const rubric = itemData.question.widgets[widgetId].options;
|
|
44935
|
+
const widgetMap = getWidgetsMapFromItemData(itemData);
|
|
44936
|
+
const widgetType = getWidgetTypeByWidgetId(widgetId, widgetMap);
|
|
44765
44937
|
const widget = getWidget(widgetType);
|
|
44766
44938
|
|
|
44767
44939
|
// @ts-expect-error - TS2339 - Property 'getOneCorrectAnswerFromRubric' does not exist on type 'ComponentType<any>'.
|
|
44768
44940
|
return widget?.getOneCorrectAnswerFromRubric?.(rubric);
|
|
44769
44941
|
};
|
|
44770
44942
|
|
|
44771
|
-
/* Verify if the widget
|
|
44772
|
-
const
|
|
44773
|
-
return perseusItem.question.content.indexOf(
|
|
44943
|
+
/* Verify if the widget ID exists in the content string of the Perseus Item */
|
|
44944
|
+
const isWidgetIdInContent = (perseusItem, widgetId) => {
|
|
44945
|
+
return perseusItem.question.content.indexOf(widgetId) !== -1;
|
|
44774
44946
|
};
|
|
44775
44947
|
|
|
44776
|
-
/* Return an array of all the widget
|
|
44777
|
-
const
|
|
44948
|
+
/* Return an array of all the widget IDs that exist in the content string of a Perseus Item */
|
|
44949
|
+
const getValidWidgetIds = perseusItem => {
|
|
44778
44950
|
const {
|
|
44779
44951
|
widgets
|
|
44780
44952
|
} = perseusItem.question;
|
|
44781
|
-
return wonderStuffCore.keys(widgets).filter(
|
|
44953
|
+
return wonderStuffCore.keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
|
|
44782
44954
|
};
|
|
44783
44955
|
|
|
44784
44956
|
/**
|
|
@@ -44826,7 +44998,7 @@ const genericPerseusItemData = {
|
|
|
44826
44998
|
* uses a basic Perseus object and updates it with custom values as needed.
|
|
44827
44999
|
*
|
|
44828
45000
|
* @param {Partial<PerseusItem>} customFields
|
|
44829
|
-
* @returns PerseusItem
|
|
45001
|
+
* @returns {PerseusItem}
|
|
44830
45002
|
*/
|
|
44831
45003
|
function generateTestPerseusItem() {
|
|
44832
45004
|
let customFields = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
@@ -44836,6 +45008,107 @@ function generateTestPerseusItem() {
|
|
|
44836
45008
|
};
|
|
44837
45009
|
}
|
|
44838
45010
|
|
|
45011
|
+
/**
|
|
45012
|
+
* Creates an object with the bare amount
|
|
45013
|
+
* of data to be a properly typed RadioWidget
|
|
45014
|
+
*
|
|
45015
|
+
* @returns {RadioWidget} skeleton RadioWidget for testing
|
|
45016
|
+
*/
|
|
45017
|
+
function generateTestRadioWidget() {
|
|
45018
|
+
return {
|
|
45019
|
+
type: "radio",
|
|
45020
|
+
options: {
|
|
45021
|
+
choices: []
|
|
45022
|
+
}
|
|
45023
|
+
};
|
|
45024
|
+
}
|
|
45025
|
+
|
|
45026
|
+
/**
|
|
45027
|
+
* Creates an object with the bare amount
|
|
45028
|
+
* of data to be a properly typed InteractiveGraphWidget
|
|
45029
|
+
*
|
|
45030
|
+
* @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
|
|
45031
|
+
*/
|
|
45032
|
+
function generateTestInteractiveGraphWidget() {
|
|
45033
|
+
return {
|
|
45034
|
+
type: "interactive-graph",
|
|
45035
|
+
options: {
|
|
45036
|
+
step: [1, 1],
|
|
45037
|
+
gridStep: [1, 1],
|
|
45038
|
+
snapStep: [1, 1],
|
|
45039
|
+
markings: "graph",
|
|
45040
|
+
labels: ["x", "y"],
|
|
45041
|
+
showProtractor: false,
|
|
45042
|
+
showRuler: false,
|
|
45043
|
+
rulerLabel: "mm",
|
|
45044
|
+
rulerTicks: 2,
|
|
45045
|
+
range: [[-10, 10], [-10, 10]],
|
|
45046
|
+
graph: {
|
|
45047
|
+
type: "angle"
|
|
45048
|
+
},
|
|
45049
|
+
correct: {
|
|
45050
|
+
type: "angle"
|
|
45051
|
+
}
|
|
45052
|
+
}
|
|
45053
|
+
};
|
|
45054
|
+
}
|
|
45055
|
+
|
|
45056
|
+
/**
|
|
45057
|
+
* Creates an object with the bare amount
|
|
45058
|
+
* of data to be a properly typed InteractiveGraphWidget
|
|
45059
|
+
*
|
|
45060
|
+
* @returns {InteractiveGraphWidget} skeleton InteractiveGraphWidget for testing
|
|
45061
|
+
*/
|
|
45062
|
+
function generateTestCategorizerWidget() {
|
|
45063
|
+
return {
|
|
45064
|
+
type: "categorizer",
|
|
45065
|
+
options: {
|
|
45066
|
+
items: [],
|
|
45067
|
+
categories: [],
|
|
45068
|
+
randomizeItems: false,
|
|
45069
|
+
static: false,
|
|
45070
|
+
values: []
|
|
45071
|
+
}
|
|
45072
|
+
};
|
|
45073
|
+
}
|
|
45074
|
+
|
|
45075
|
+
/**
|
|
45076
|
+
* Creates an object with the bare amount
|
|
45077
|
+
* of data to be a properly typed ExpressionWidget
|
|
45078
|
+
*
|
|
45079
|
+
* @returns {ExpressionWidget} skeleton ExpressionWidget for testing
|
|
45080
|
+
*/
|
|
45081
|
+
function generateTestExpressionWidget() {
|
|
45082
|
+
return {
|
|
45083
|
+
type: "expression",
|
|
45084
|
+
options: {
|
|
45085
|
+
answerForms: [],
|
|
45086
|
+
buttonSets: ["basic"],
|
|
45087
|
+
functions: [],
|
|
45088
|
+
times: false
|
|
45089
|
+
}
|
|
45090
|
+
};
|
|
45091
|
+
}
|
|
45092
|
+
|
|
45093
|
+
/**
|
|
45094
|
+
* Creates an object with the bare amount
|
|
45095
|
+
* of data to be a properly typed NumericInputWidget
|
|
45096
|
+
*
|
|
45097
|
+
* @returns {NumericInputWidget} skeleton NumericInputWidget for testing
|
|
45098
|
+
*/
|
|
45099
|
+
function generateTestNumericInputWidget() {
|
|
45100
|
+
return {
|
|
45101
|
+
type: "numeric-input",
|
|
45102
|
+
options: {
|
|
45103
|
+
answers: [],
|
|
45104
|
+
labelText: "",
|
|
45105
|
+
size: "normal",
|
|
45106
|
+
coefficient: false,
|
|
45107
|
+
static: false
|
|
45108
|
+
}
|
|
45109
|
+
};
|
|
45110
|
+
}
|
|
45111
|
+
|
|
44839
45112
|
/**
|
|
44840
45113
|
* Some helpers for CEDAR logging. CEDAR convention dictates
|
|
44841
45114
|
* that enum values should be upper case. Ex: for the
|
|
@@ -44890,162 +45163,6 @@ function convertWidgetNameToEnum(name) {
|
|
|
44890
45163
|
}
|
|
44891
45164
|
return widgetEnum;
|
|
44892
45165
|
}
|
|
44893
|
-
function getWidgetTypeFromWidgetId(widgetId) {
|
|
44894
|
-
const [widgetType, widgetNumber] = widgetId.split(" ");
|
|
44895
|
-
if (!widgetNumber) {
|
|
44896
|
-
throw new Error(`Incorrect format for widget ID: ${widgetId}`);
|
|
44897
|
-
}
|
|
44898
|
-
if (!widgetNameToEnum[widgetType]) {
|
|
44899
|
-
throw new Error(`Unknown widget type (${widgetType}) in widget ID: ${widgetId}`);
|
|
44900
|
-
}
|
|
44901
|
-
return widgetType;
|
|
44902
|
-
}
|
|
44903
|
-
|
|
44904
|
-
let WidgetType = /*#__PURE__*/function (WidgetType) {
|
|
44905
|
-
WidgetType["Categorizer"] = "categorizer";
|
|
44906
|
-
WidgetType["CSProgram"] = "cs-program";
|
|
44907
|
-
WidgetType["Definition"] = "definition";
|
|
44908
|
-
WidgetType["Dropdown"] = "dropdown";
|
|
44909
|
-
WidgetType["Explanation"] = "explanation";
|
|
44910
|
-
WidgetType["Expression"] = "expression";
|
|
44911
|
-
WidgetType["GradedGroup"] = "graded-group";
|
|
44912
|
-
WidgetType["GradedGroupAnswerBar"] = "graded-group-answer-bar";
|
|
44913
|
-
WidgetType["GradedGroupSet"] = "graded-group-set";
|
|
44914
|
-
WidgetType["Grapher"] = "grapher";
|
|
44915
|
-
WidgetType["Group"] = "group";
|
|
44916
|
-
WidgetType["Iframe"] = "iframe";
|
|
44917
|
-
WidgetType["Image"] = "image";
|
|
44918
|
-
WidgetType["InputNumber"] = "input-number";
|
|
44919
|
-
WidgetType["Interaction"] = "interaction";
|
|
44920
|
-
WidgetType["InteractiveGraph"] = "interactive-graph";
|
|
44921
|
-
WidgetType["InteractiveNumberLine"] = "interactive-number-line";
|
|
44922
|
-
WidgetType["LabelImage"] = "label-image";
|
|
44923
|
-
WidgetType["Matcher"] = "matcher";
|
|
44924
|
-
WidgetType["Matrix"] = "matrix";
|
|
44925
|
-
WidgetType["Measurer"] = "measurer";
|
|
44926
|
-
WidgetType["Molecule"] = "molecule";
|
|
44927
|
-
WidgetType["NumberLine"] = "number-line";
|
|
44928
|
-
WidgetType["NumericInput"] = "numeric-input";
|
|
44929
|
-
WidgetType["Orderer"] = "orderer";
|
|
44930
|
-
WidgetType["Passage"] = "passage";
|
|
44931
|
-
WidgetType["PassageRef"] = "passage-ref";
|
|
44932
|
-
WidgetType["PassageRefTarget"] = "passage-ref-target";
|
|
44933
|
-
WidgetType["Plotter"] = "plotter";
|
|
44934
|
-
WidgetType["Protractor"] = "protractor";
|
|
44935
|
-
WidgetType["PythonProgram"] = "python-program";
|
|
44936
|
-
WidgetType["Radio"] = "radio";
|
|
44937
|
-
WidgetType["Sorter"] = "sorter";
|
|
44938
|
-
WidgetType["Table"] = "table";
|
|
44939
|
-
WidgetType["Unit"] = "unit";
|
|
44940
|
-
WidgetType["Video"] = "video";
|
|
44941
|
-
WidgetType["VideoTranscriptLink"] = "video-transcript-link";
|
|
44942
|
-
return WidgetType;
|
|
44943
|
-
}({});
|
|
44944
|
-
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
44945
|
-
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
44946
|
-
|
|
44947
|
-
/**
|
|
44948
|
-
* Regex for widget placeholders in a string.
|
|
44949
|
-
*
|
|
44950
|
-
* First capture group is the widget ID (ex. 'radio 1')
|
|
44951
|
-
* Second capture group is the widget type (ex. "radio)
|
|
44952
|
-
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
44953
|
-
*/
|
|
44954
|
-
function getWidgetRegex() {
|
|
44955
|
-
return /\[\[☃ (([a-z-]+) \d+)\]\]/g;
|
|
44956
|
-
}
|
|
44957
|
-
|
|
44958
|
-
/**
|
|
44959
|
-
* Add a widget placeholder using the widget ID.
|
|
44960
|
-
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
44961
|
-
*
|
|
44962
|
-
* @param {string} id
|
|
44963
|
-
* @returns {string}
|
|
44964
|
-
*/
|
|
44965
|
-
function addWidget(id) {
|
|
44966
|
-
return `[[☃ ${id}]]`;
|
|
44967
|
-
}
|
|
44968
|
-
|
|
44969
|
-
/**
|
|
44970
|
-
* Extract all widget IDs, which includes the widget type and instance number.
|
|
44971
|
-
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
44972
|
-
*
|
|
44973
|
-
* Content should contain Perseus widget placeholders,
|
|
44974
|
-
* which look like: '[[☃ radio 1]]'.
|
|
44975
|
-
*
|
|
44976
|
-
* @param {string} content
|
|
44977
|
-
* @returns {Array<WidgetId>} widgets
|
|
44978
|
-
*/
|
|
44979
|
-
function getAllWidgetIds(content) {
|
|
44980
|
-
const widgets = [];
|
|
44981
|
-
const localWidgetRegex = getWidgetRegex();
|
|
44982
|
-
let match = localWidgetRegex.exec(content);
|
|
44983
|
-
while (match !== null) {
|
|
44984
|
-
widgets.push(match[1]);
|
|
44985
|
-
match = localWidgetRegex.exec(content);
|
|
44986
|
-
}
|
|
44987
|
-
return widgets;
|
|
44988
|
-
}
|
|
44989
|
-
|
|
44990
|
-
/**
|
|
44991
|
-
* Check if a specific widget is an actual widget type using its name.
|
|
44992
|
-
* The widget name should include a widget type, without an instance number,
|
|
44993
|
-
* as a string. ex. 'radio'
|
|
44994
|
-
*
|
|
44995
|
-
* @param {string} widgetName
|
|
44996
|
-
* @returns {boolean}
|
|
44997
|
-
*/
|
|
44998
|
-
function isWidgetType(widgetName) {
|
|
44999
|
-
return Object.values(WidgetType).includes(widgetName);
|
|
45000
|
-
}
|
|
45001
|
-
|
|
45002
|
-
/**
|
|
45003
|
-
* Extract all widget types from a Perseus JSON content string.
|
|
45004
|
-
* This does not include the instance number and prevents duplicates.
|
|
45005
|
-
* example output: ['radio', 'categorizer']
|
|
45006
|
-
*
|
|
45007
|
-
* @param {string} content
|
|
45008
|
-
* @returns {Array<WidgetId>}
|
|
45009
|
-
*/
|
|
45010
|
-
function getAllWidgetTypes(content) {
|
|
45011
|
-
const widgetTypes = [];
|
|
45012
|
-
const localWidgetRegex = getWidgetRegex();
|
|
45013
|
-
let match = localWidgetRegex.exec(content);
|
|
45014
|
-
while (match !== null) {
|
|
45015
|
-
const matchType = match[2];
|
|
45016
|
-
if (!widgetTypes.includes(matchType) && isWidgetType(matchType)) {
|
|
45017
|
-
widgetTypes.push(matchType);
|
|
45018
|
-
}
|
|
45019
|
-
match = localWidgetRegex.exec(content);
|
|
45020
|
-
}
|
|
45021
|
-
return widgetTypes;
|
|
45022
|
-
}
|
|
45023
|
-
|
|
45024
|
-
/**
|
|
45025
|
-
* Check if a specific widget is a question widget type using its ID.
|
|
45026
|
-
* The widget ID includes the widget type and the instance number.
|
|
45027
|
-
*
|
|
45028
|
-
* @param {WidgetId} widgetId
|
|
45029
|
-
* @returns {boolean}
|
|
45030
|
-
*/
|
|
45031
|
-
function isQuestionWidgetType(widgetId) {
|
|
45032
|
-
return QUESTION_WIDGETS.includes(widgetId.split(" ")[0]);
|
|
45033
|
-
}
|
|
45034
|
-
|
|
45035
|
-
/**
|
|
45036
|
-
* Extract the widget IDs of all widgets considered question widgets. Widget IDs
|
|
45037
|
-
* include widget type and instance number.
|
|
45038
|
-
* example output: ['radio 1', 'categorizer 1']
|
|
45039
|
-
*
|
|
45040
|
-
* Content should contain Perseus widget placeholders,
|
|
45041
|
-
* which look like: '[[☃ radio 1]]'.
|
|
45042
|
-
*
|
|
45043
|
-
* @param {string} content
|
|
45044
|
-
* @returns Array<WidgetId>
|
|
45045
|
-
*/
|
|
45046
|
-
function getQuestionWidgetIds(content) {
|
|
45047
|
-
return getAllWidgetIds(content).filter(isQuestionWidgetType);
|
|
45048
|
-
}
|
|
45049
45166
|
|
|
45050
45167
|
const EditorJsonify = {
|
|
45051
45168
|
serialize: function () {
|
|
@@ -45097,7 +45214,6 @@ exports.TableWidget = Table$1;
|
|
|
45097
45214
|
exports.Util = Util;
|
|
45098
45215
|
exports.WIDGET_PROP_DENYLIST = denylist;
|
|
45099
45216
|
exports.WidgetJsonifyDeprecated = WidgetJsonifyDeprecated;
|
|
45100
|
-
exports.WidgetType = WidgetType;
|
|
45101
45217
|
exports.Widgets = widgets$1;
|
|
45102
45218
|
exports.addWidget = addWidget;
|
|
45103
45219
|
exports.apiVersion = apiVersion;
|
|
@@ -45105,19 +45221,26 @@ exports.buildEmptyItemTreeForShape = buildEmptyItemTreeForShape;
|
|
|
45105
45221
|
exports.buildMapper = buildMapper;
|
|
45106
45222
|
exports.components = components;
|
|
45107
45223
|
exports.constants = constants;
|
|
45224
|
+
exports.contentHasWidgetType = contentHasWidgetType;
|
|
45108
45225
|
exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
|
|
45109
45226
|
exports.displaySigFigs = displaySigFigs;
|
|
45227
|
+
exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
|
|
45228
|
+
exports.generateTestExpressionWidget = generateTestExpressionWidget;
|
|
45229
|
+
exports.generateTestInteractiveGraphWidget = generateTestInteractiveGraphWidget;
|
|
45230
|
+
exports.generateTestNumericInputWidget = generateTestNumericInputWidget;
|
|
45110
45231
|
exports.generateTestPerseusItem = generateTestPerseusItem;
|
|
45232
|
+
exports.generateTestRadioWidget = generateTestRadioWidget;
|
|
45111
45233
|
exports.genericPerseusItemData = genericPerseusItemData;
|
|
45112
|
-
exports.getAllWidgetIds = getAllWidgetIds;
|
|
45113
|
-
exports.getAllWidgetTypes = getAllWidgetTypes;
|
|
45114
45234
|
exports.getAnswerFromUserInput = getAnswerFromUserInput;
|
|
45115
45235
|
exports.getAnswersFromWidgets = getAnswersFromWidgets;
|
|
45116
|
-
exports.
|
|
45117
|
-
exports.
|
|
45118
|
-
exports.
|
|
45119
|
-
exports.
|
|
45120
|
-
exports.
|
|
45236
|
+
exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
|
|
45237
|
+
exports.getValidWidgetIds = getValidWidgetIds;
|
|
45238
|
+
exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
|
|
45239
|
+
exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
|
|
45240
|
+
exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
|
|
45241
|
+
exports.getWidgetTypeByWidgetId = getWidgetTypeByWidgetId;
|
|
45242
|
+
exports.getWidgetsFromWidgetMap = getWidgetsFromWidgetMap;
|
|
45243
|
+
exports.getWidgetsMapFromItemData = getWidgetsMapFromItemData;
|
|
45121
45244
|
exports.globalConstants = globalConstants;
|
|
45122
45245
|
exports.globalStyles = globalStyles;
|
|
45123
45246
|
exports.icons = iconPaths;
|
|
@@ -45125,7 +45248,7 @@ exports.init = init;
|
|
|
45125
45248
|
exports.injectWidgets = injectWidgets;
|
|
45126
45249
|
exports.interactiveSizes = interactiveSizes$1;
|
|
45127
45250
|
exports.isItemRenderableByVersion = isItemRenderableByVersion;
|
|
45128
|
-
exports.
|
|
45251
|
+
exports.isWidgetIdInContent = isWidgetIdInContent;
|
|
45129
45252
|
exports.isWrongAnswerSupported = isWrongAnswerSupported;
|
|
45130
45253
|
exports.itemDataVersion = itemDataVersion;
|
|
45131
45254
|
exports.itemToTree = itemToTree;
|