@khanacademy/perseus 26.1.0 → 27.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/es/index.css +0 -10
- package/dist/es/index.js +2738 -3018
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +0 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1461 -1745
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +25 -12
- package/dist/widgets/explanation.d.ts +3 -0
- package/dist/widgets/interactive-graph.d.ts +0 -3
- package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +2 -1
- package/dist/widgets/interactive-graphs/reducer/initialize-graph-state.d.ts +3 -1
- package/dist/widgets/interactive-graphs/reducer/use-graph-config.d.ts +1 -0
- package/dist/widgets/label-image/marker.d.ts +3 -0
- package/dist/widgets/label-image.d.ts +3 -0
- package/package.json +4 -4
- package/dist/widgets/unit.d.ts +0 -85
package/dist/index.js
CHANGED
|
@@ -19195,6 +19195,7 @@ const replaceDeprecatedWidgets = () => {
|
|
|
19195
19195
|
replaceWidget("reaction-diagram", "deprecated-standin");
|
|
19196
19196
|
replaceWidget("sequence", "deprecated-standin");
|
|
19197
19197
|
replaceWidget("simulator", "deprecated-standin");
|
|
19198
|
+
replaceWidget("unit-input", "deprecated-standin");
|
|
19198
19199
|
};
|
|
19199
19200
|
const registerEditors = editorsToRegister => {
|
|
19200
19201
|
editorsToRegister.forEach(editor => {
|
|
@@ -19228,6 +19229,7 @@ const replaceDeprecatedEditors = () => {
|
|
|
19228
19229
|
replaceEditor("reaction-diagram", "deprecated-standin");
|
|
19229
19230
|
replaceEditor("sequence", "deprecated-standin");
|
|
19230
19231
|
replaceEditor("simulator", "deprecated-standin");
|
|
19232
|
+
replaceEditor("unit-input", "deprecated-standin");
|
|
19231
19233
|
};
|
|
19232
19234
|
const getWidget = name => {
|
|
19233
19235
|
// TODO(alex): Consider referring to these as renderers to avoid
|
|
@@ -25294,6 +25296,13 @@ class Explanation extends React__namespace.Component {
|
|
|
25294
25296
|
state = {
|
|
25295
25297
|
expanded: false
|
|
25296
25298
|
};
|
|
25299
|
+
_mounted = false;
|
|
25300
|
+
componentDidMount() {
|
|
25301
|
+
this._mounted = true;
|
|
25302
|
+
}
|
|
25303
|
+
componentWillUnmount() {
|
|
25304
|
+
this._mounted = false;
|
|
25305
|
+
}
|
|
25297
25306
|
change = (() => {
|
|
25298
25307
|
var _this = this;
|
|
25299
25308
|
return function () {
|
|
@@ -25312,7 +25321,7 @@ class Explanation extends React__namespace.Component {
|
|
|
25312
25321
|
render() {
|
|
25313
25322
|
const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
|
|
25314
25323
|
const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
|
|
25315
|
-
const allowTransition = mediaQueryIsMatched("(prefers-reduced-motion: no-preference)");
|
|
25324
|
+
const allowTransition = this._mounted && mediaQueryIsMatched("(prefers-reduced-motion: no-preference)");
|
|
25316
25325
|
|
|
25317
25326
|
// Special styling is needed to fit the button in a block of text without throwing off the line spacing.
|
|
25318
25327
|
// While the button is not normally included in a block of text, it needs to be able to accommodate such a case.
|
|
@@ -28588,7 +28597,8 @@ const defaultGraphConfig = {
|
|
|
28588
28597
|
graphDimensionsInPixels: [1, 1],
|
|
28589
28598
|
width: 0,
|
|
28590
28599
|
height: 0,
|
|
28591
|
-
labels: []
|
|
28600
|
+
labels: [],
|
|
28601
|
+
hintMode: false
|
|
28592
28602
|
};
|
|
28593
28603
|
const GraphConfigContext = /*#__PURE__*/React.createContext(defaultGraphConfig);
|
|
28594
28604
|
function useGraphConfig() {
|
|
@@ -30073,8 +30083,6 @@ const ItemExtras = [
|
|
|
30073
30083
|
|
|
30074
30084
|
// prettier-ignore
|
|
30075
30085
|
|
|
30076
|
-
// prettier-ignore
|
|
30077
|
-
|
|
30078
30086
|
//prettier-ignore
|
|
30079
30087
|
|
|
30080
30088
|
// A background image applied to various widgets.
|
|
@@ -30600,6 +30608,16 @@ function useDraggable(args) {
|
|
|
30600
30608
|
const isKeyboard = type.includes("key");
|
|
30601
30609
|
if (isKeyboard) {
|
|
30602
30610
|
event?.preventDefault();
|
|
30611
|
+
|
|
30612
|
+
// When a key is held down, we see multiple "keydown" events,
|
|
30613
|
+
// followed by a single "keyup" event.
|
|
30614
|
+
// For a single keypress, we only see a "keydown" event, no
|
|
30615
|
+
// "keyup".
|
|
30616
|
+
// We never want to process the keyup event as an intent to
|
|
30617
|
+
// move so we bail on further processing here.
|
|
30618
|
+
if (type === "keyup") {
|
|
30619
|
+
return;
|
|
30620
|
+
}
|
|
30603
30621
|
const {
|
|
30604
30622
|
direction: yDownDirection,
|
|
30605
30623
|
altKey,
|
|
@@ -30700,7 +30718,8 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
|
|
|
30700
30718
|
const {
|
|
30701
30719
|
range,
|
|
30702
30720
|
markings,
|
|
30703
|
-
showTooltips
|
|
30721
|
+
showTooltips,
|
|
30722
|
+
hintMode
|
|
30704
30723
|
} = useGraphConfig();
|
|
30705
30724
|
const {
|
|
30706
30725
|
point,
|
|
@@ -30745,7 +30764,7 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
|
|
|
30745
30764
|
cursor
|
|
30746
30765
|
},
|
|
30747
30766
|
"data-testid": "movable-point",
|
|
30748
|
-
tabIndex: tabIndex(focusBehavior)
|
|
30767
|
+
tabIndex: hintMode ? -1 : tabIndex(focusBehavior)
|
|
30749
30768
|
}, /*#__PURE__*/React__namespace.createElement("circle", {
|
|
30750
30769
|
className: "movable-point-hitbox",
|
|
30751
30770
|
r: hitboxSizePx / 2,
|
|
@@ -30833,7 +30852,8 @@ const MovableLine = props => {
|
|
|
30833
30852
|
};
|
|
30834
30853
|
function useControlPoint(point, color, onMovePoint) {
|
|
30835
30854
|
const {
|
|
30836
|
-
snapStep
|
|
30855
|
+
snapStep,
|
|
30856
|
+
hintMode
|
|
30837
30857
|
} = useGraphConfig();
|
|
30838
30858
|
const [focused, setFocused] = React.useState(false);
|
|
30839
30859
|
const keyboardHandleRef = React.useRef(null);
|
|
@@ -30855,7 +30875,7 @@ function useControlPoint(point, color, onMovePoint) {
|
|
|
30855
30875
|
const focusableHandle = /*#__PURE__*/React__namespace.createElement("g", {
|
|
30856
30876
|
"data-testid": "movable-point__focusable-handle",
|
|
30857
30877
|
className: "movable-point__focusable-handle",
|
|
30858
|
-
tabIndex: 0,
|
|
30878
|
+
tabIndex: hintMode ? -1 : 0,
|
|
30859
30879
|
ref: keyboardHandleRef,
|
|
30860
30880
|
onFocus: () => setFocused(true),
|
|
30861
30881
|
onBlur: () => setFocused(false)
|
|
@@ -30888,7 +30908,8 @@ const Line$1 = props => {
|
|
|
30888
30908
|
const {
|
|
30889
30909
|
range,
|
|
30890
30910
|
graphDimensionsInPixels,
|
|
30891
|
-
snapStep
|
|
30911
|
+
snapStep,
|
|
30912
|
+
hintMode
|
|
30892
30913
|
} = useGraphConfig();
|
|
30893
30914
|
let startExtend = undefined;
|
|
30894
30915
|
let endExtend = undefined;
|
|
@@ -30910,7 +30931,7 @@ const Line$1 = props => {
|
|
|
30910
30931
|
});
|
|
30911
30932
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("g", {
|
|
30912
30933
|
ref: line,
|
|
30913
|
-
tabIndex: 0,
|
|
30934
|
+
tabIndex: hintMode ? -1 : 0,
|
|
30914
30935
|
className: "movable-line",
|
|
30915
30936
|
"data-testid": "movable-line",
|
|
30916
30937
|
style: {
|
|
@@ -31429,6 +31450,9 @@ const PolygonGraph = props => {
|
|
|
31429
31450
|
snapStep,
|
|
31430
31451
|
snapTo
|
|
31431
31452
|
} = props.graphState;
|
|
31453
|
+
const {
|
|
31454
|
+
hintMode
|
|
31455
|
+
} = useGraphConfig();
|
|
31432
31456
|
|
|
31433
31457
|
// TODO(benchristel): can the default set of points be removed here? I don't
|
|
31434
31458
|
// think coords can be null.
|
|
@@ -31448,12 +31472,16 @@ const PolygonGraph = props => {
|
|
|
31448
31472
|
constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
|
|
31449
31473
|
});
|
|
31450
31474
|
const active = hovered || focused || dragging;
|
|
31475
|
+
const lastMoveTime = React__namespace.useRef(0);
|
|
31451
31476
|
const lines = getLines(points);
|
|
31452
31477
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
|
|
31453
31478
|
points: [...points],
|
|
31454
31479
|
color: "var(--movable-line-stroke-color)",
|
|
31455
31480
|
svgPolygonProps: {
|
|
31456
|
-
strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)"
|
|
31481
|
+
strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
|
|
31482
|
+
style: {
|
|
31483
|
+
fill: "transparent"
|
|
31484
|
+
}
|
|
31457
31485
|
}
|
|
31458
31486
|
}), points.map((point, i) => {
|
|
31459
31487
|
const pt1 = points.at(i - 1);
|
|
@@ -31485,10 +31513,11 @@ const PolygonGraph = props => {
|
|
|
31485
31513
|
color: "transparent",
|
|
31486
31514
|
svgPolygonProps: {
|
|
31487
31515
|
ref,
|
|
31488
|
-
tabIndex: 0,
|
|
31516
|
+
tabIndex: hintMode ? -1 : 0,
|
|
31489
31517
|
strokeWidth: TARGET_SIZE,
|
|
31490
31518
|
style: {
|
|
31491
|
-
cursor: dragging ? "grabbing" : "grab"
|
|
31519
|
+
cursor: dragging ? "grabbing" : "grab",
|
|
31520
|
+
fill: hovered ? "var(--mafs-blue)" : "transparent"
|
|
31492
31521
|
},
|
|
31493
31522
|
onFocus: () => setFocused(true),
|
|
31494
31523
|
onBlur: () => setFocused(false),
|
|
@@ -31500,7 +31529,15 @@ const PolygonGraph = props => {
|
|
|
31500
31529
|
key: "point-" + i,
|
|
31501
31530
|
snapTo: snapTo,
|
|
31502
31531
|
point: point,
|
|
31503
|
-
onMove: destination =>
|
|
31532
|
+
onMove: destination => {
|
|
31533
|
+
const now = Date.now();
|
|
31534
|
+
const targetFPS = 40;
|
|
31535
|
+
const moveThresholdTime = 1000 / targetFPS;
|
|
31536
|
+
if (now - lastMoveTime.current > moveThresholdTime) {
|
|
31537
|
+
dispatch(actions.polygon.movePoint(i, destination));
|
|
31538
|
+
lastMoveTime.current = now;
|
|
31539
|
+
}
|
|
31540
|
+
}
|
|
31504
31541
|
})));
|
|
31505
31542
|
};
|
|
31506
31543
|
function getLines(points) {
|
|
@@ -31621,7 +31658,8 @@ function MovableCircle(props) {
|
|
|
31621
31658
|
onMove
|
|
31622
31659
|
} = props;
|
|
31623
31660
|
const {
|
|
31624
|
-
snapStep
|
|
31661
|
+
snapStep,
|
|
31662
|
+
hintMode
|
|
31625
31663
|
} = useGraphConfig();
|
|
31626
31664
|
const draggableRef = React.useRef(null);
|
|
31627
31665
|
const {
|
|
@@ -31636,7 +31674,7 @@ function MovableCircle(props) {
|
|
|
31636
31674
|
const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
|
|
31637
31675
|
return /*#__PURE__*/React__namespace.createElement("g", {
|
|
31638
31676
|
ref: draggableRef,
|
|
31639
|
-
tabIndex: 0,
|
|
31677
|
+
tabIndex: hintMode ? -1 : 0,
|
|
31640
31678
|
className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
|
|
31641
31679
|
}, /*#__PURE__*/React__namespace.createElement("ellipse", {
|
|
31642
31680
|
className: "focus-ring",
|
|
@@ -32040,7 +32078,8 @@ const MafsGraph = props => {
|
|
|
32040
32078
|
const {
|
|
32041
32079
|
state,
|
|
32042
32080
|
dispatch,
|
|
32043
|
-
labels
|
|
32081
|
+
labels,
|
|
32082
|
+
hintMode
|
|
32044
32083
|
} = props;
|
|
32045
32084
|
const [width, height] = props.box;
|
|
32046
32085
|
const tickStep = props.step;
|
|
@@ -32055,7 +32094,8 @@ const MafsGraph = props => {
|
|
|
32055
32094
|
graphDimensionsInPixels: props.box,
|
|
32056
32095
|
width,
|
|
32057
32096
|
height,
|
|
32058
|
-
labels
|
|
32097
|
+
labels,
|
|
32098
|
+
hintMode
|
|
32059
32099
|
}
|
|
32060
32100
|
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
32061
32101
|
className: "mafs-graph",
|
|
@@ -32066,7 +32106,8 @@ const MafsGraph = props => {
|
|
|
32066
32106
|
padding: "25px 25px 0 0",
|
|
32067
32107
|
boxSizing: "content-box",
|
|
32068
32108
|
marginLeft: "20px",
|
|
32069
|
-
marginBottom: "20px"
|
|
32109
|
+
marginBottom: "20px",
|
|
32110
|
+
pointerEvents: hintMode ? "none" : "auto"
|
|
32070
32111
|
}
|
|
32071
32112
|
}, /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
|
|
32072
32113
|
box: props.box,
|
|
@@ -32097,7 +32138,7 @@ const MafsGraph = props => {
|
|
|
32097
32138
|
}), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
|
|
32098
32139
|
lockedFigures: props.lockedFigures,
|
|
32099
32140
|
range: state.range
|
|
32100
|
-
}),
|
|
32141
|
+
}), props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
|
|
32101
32142
|
state,
|
|
32102
32143
|
dispatch
|
|
32103
32144
|
})))));
|
|
@@ -33198,7 +33239,6 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
|
|
|
33198
33239
|
const snapTo = graph.type === "polygon" ? graph.snapTo : null;
|
|
33199
33240
|
const showAngles = graph.type === "polygon" ? graph.showAngles : null;
|
|
33200
33241
|
const showSides = graph.type === "polygon" ? graph.showSides : null;
|
|
33201
|
-
const startCoords = graph.startCoords ?? null;
|
|
33202
33242
|
const originalPropsRef = React.useRef(props);
|
|
33203
33243
|
const latestPropsRef = wonderBlocksCore.useLatestRef(props);
|
|
33204
33244
|
React.useEffect(() => {
|
|
@@ -33209,7 +33249,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
|
|
|
33209
33249
|
if (latestPropsRef.current !== originalPropsRef.current) {
|
|
33210
33250
|
dispatch(reinitialize(latestPropsRef.current));
|
|
33211
33251
|
}
|
|
33212
|
-
}, [graph.type, numSegments, numSides, snapTo, showAngles, showSides,
|
|
33252
|
+
}, [graph.type, numSegments, numSides, snapTo, showAngles, showSides, latestPropsRef, graph.startCoords]);
|
|
33213
33253
|
return /*#__PURE__*/React__namespace.createElement(MafsGraph, _extends({}, props, {
|
|
33214
33254
|
state: state,
|
|
33215
33255
|
dispatch: dispatch
|
|
@@ -33300,9 +33340,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
|
|
|
33300
33340
|
markings: "graph",
|
|
33301
33341
|
showTooltips: false,
|
|
33302
33342
|
showProtractor: false,
|
|
33303
|
-
showRuler: false,
|
|
33304
|
-
rulerLabel: "",
|
|
33305
|
-
rulerTicks: 10,
|
|
33306
33343
|
graph: {
|
|
33307
33344
|
type: "linear"
|
|
33308
33345
|
}
|
|
@@ -34465,9 +34502,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
|
|
|
34465
34502
|
markings: this.props.markings,
|
|
34466
34503
|
backgroundImage: this.props.backgroundImage,
|
|
34467
34504
|
showProtractor: this.props.showProtractor,
|
|
34468
|
-
showRuler: this.props.showRuler,
|
|
34469
|
-
rulerLabel: this.props.rulerLabel,
|
|
34470
|
-
rulerTicks: this.props.rulerTicks,
|
|
34471
34505
|
onMouseDown: onMouseDown,
|
|
34472
34506
|
onGraphieUpdated: this.setGraphie,
|
|
34473
34507
|
setDrawingAreaAvailable: this.props.apiOptions.setDrawingAreaAvailable,
|
|
@@ -34486,9 +34520,6 @@ class InteractiveGraph extends React__namespace.Component {
|
|
|
34486
34520
|
markings: "graph",
|
|
34487
34521
|
showTooltips: false,
|
|
34488
34522
|
showProtractor: false,
|
|
34489
|
-
showRuler: false,
|
|
34490
|
-
rulerLabel: "",
|
|
34491
|
-
rulerTicks: 10,
|
|
34492
34523
|
graph: {
|
|
34493
34524
|
type: "linear"
|
|
34494
34525
|
}
|
|
@@ -36226,6 +36257,10 @@ const styles$f = aphrodite.StyleSheet.create({
|
|
|
36226
36257
|
|
|
36227
36258
|
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
36228
36259
|
function shouldReduceMotion() {
|
|
36260
|
+
// We cannot use matchMedia during SSR.
|
|
36261
|
+
if (typeof window.matchMedia !== "function") {
|
|
36262
|
+
return true;
|
|
36263
|
+
}
|
|
36229
36264
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
36230
36265
|
return !mediaQuery || mediaQuery.matches;
|
|
36231
36266
|
}
|
|
@@ -36234,9 +36269,16 @@ class Marker extends React__namespace.Component {
|
|
|
36234
36269
|
static contextType = PerseusI18nContext;
|
|
36235
36270
|
// The marker icon element.
|
|
36236
36271
|
_icon;
|
|
36272
|
+
_mounted = false;
|
|
36237
36273
|
static defaultProps = {
|
|
36238
36274
|
selected: []
|
|
36239
36275
|
};
|
|
36276
|
+
componentDidMount() {
|
|
36277
|
+
this._mounted = true;
|
|
36278
|
+
}
|
|
36279
|
+
componentWillUnmount() {
|
|
36280
|
+
this._mounted = false;
|
|
36281
|
+
}
|
|
36240
36282
|
renderIcon() {
|
|
36241
36283
|
const {
|
|
36242
36284
|
selected,
|
|
@@ -36279,7 +36321,7 @@ class Marker extends React__namespace.Component {
|
|
|
36279
36321
|
size: 8
|
|
36280
36322
|
};
|
|
36281
36323
|
} else if (showPulsate) {
|
|
36282
|
-
iconStyles = [styles$e.markerPulsateBase, shouldReduceMotion() ? showPulsate && styles$e.markerUnfilledPulsateOnce : showPulsate && styles$e.markerUnfilledPulsateInfinite];
|
|
36324
|
+
iconStyles = [styles$e.markerPulsateBase, this._mounted && shouldReduceMotion() ? showPulsate && styles$e.markerUnfilledPulsateOnce : showPulsate && styles$e.markerUnfilledPulsateInfinite];
|
|
36283
36325
|
}
|
|
36284
36326
|
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
36285
36327
|
style: [styles$e.markerIcon, iconStyles],
|
|
@@ -36407,6 +36449,7 @@ class LabelImage extends React__namespace.Component {
|
|
|
36407
36449
|
static contextType = PerseusI18nContext;
|
|
36408
36450
|
// The rendered markers on the question image for labeling.
|
|
36409
36451
|
_markers;
|
|
36452
|
+
_mounted = false;
|
|
36410
36453
|
static gradeMarker(marker) {
|
|
36411
36454
|
const score = {
|
|
36412
36455
|
hasAnswers: false,
|
|
@@ -36644,6 +36687,12 @@ class LabelImage extends React__namespace.Component {
|
|
|
36644
36687
|
hideAnswers: false
|
|
36645
36688
|
};
|
|
36646
36689
|
}
|
|
36690
|
+
componentDidMount() {
|
|
36691
|
+
this._mounted = true;
|
|
36692
|
+
}
|
|
36693
|
+
componentWillUnmount() {
|
|
36694
|
+
this._mounted = false;
|
|
36695
|
+
}
|
|
36647
36696
|
simpleValidate(rubric) {
|
|
36648
36697
|
return LabelImage.validate(this.getUserInput(), rubric);
|
|
36649
36698
|
}
|
|
@@ -36782,13 +36831,14 @@ class LabelImage extends React__namespace.Component {
|
|
|
36782
36831
|
markersInteracted
|
|
36783
36832
|
} = this.state;
|
|
36784
36833
|
|
|
36834
|
+
// Determine whether page is rendered in a narrow browser window.
|
|
36835
|
+
const isNarrowPage = this._mounted && window.matchMedia(mediaQueries.xsOrSmaller.replace("@media ", "")).matches;
|
|
36836
|
+
|
|
36837
|
+
// Determine whether the image is wider than it is tall.
|
|
36838
|
+
const isWideImage = this.props.imageWidth / 2 > this.props.imageHeight;
|
|
36839
|
+
|
|
36785
36840
|
// Render all markers for widget.
|
|
36786
36841
|
return markers.map((marker, index) => {
|
|
36787
|
-
// Determine whether page is rendered in a narrow browser window.
|
|
36788
|
-
const isNarrowPage = window.matchMedia(mediaQueries.xsOrSmaller.replace("@media ", "")).matches;
|
|
36789
|
-
|
|
36790
|
-
// Determine whether the image is wider than it is tall.
|
|
36791
|
-
const isWideImage = this.props.imageWidth / 2 > this.props.imageHeight;
|
|
36792
36842
|
let side;
|
|
36793
36843
|
let markerPosition;
|
|
36794
36844
|
// Position popup closest to the center, preferring it renders
|
|
@@ -44207,1320 +44257,405 @@ var Table$1 = {
|
|
|
44207
44257
|
isLintable: true
|
|
44208
44258
|
};
|
|
44209
44259
|
|
|
44210
|
-
|
|
44211
|
-
|
|
44260
|
+
/**
|
|
44261
|
+
* This is a component that adds a link to the transcript for embedded videos in articles.
|
|
44262
|
+
*/
|
|
44263
|
+
const IS_URL$1 = /^https?:\/\//;
|
|
44264
|
+
const getYoutubeId = url => {
|
|
44265
|
+
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
|
44266
|
+
const match = url.match(regExp);
|
|
44267
|
+
if (match && match[7].length === 11) {
|
|
44268
|
+
return match[7];
|
|
44269
|
+
}
|
|
44270
|
+
return "videoNotFound";
|
|
44212
44271
|
};
|
|
44213
|
-
|
|
44214
|
-
|
|
44215
|
-
|
|
44216
|
-
|
|
44217
|
-
|
|
44218
|
-
|
|
44219
|
-
|
|
44272
|
+
|
|
44273
|
+
// will accept a youtube link or a video's slug name from embedded video and then query accordingly.
|
|
44274
|
+
|
|
44275
|
+
/**
|
|
44276
|
+
* Video Transcript Link Component.
|
|
44277
|
+
*/
|
|
44278
|
+
const VideoTranscriptLink = props => {
|
|
44279
|
+
const {
|
|
44280
|
+
location
|
|
44281
|
+
} = props;
|
|
44282
|
+
const {
|
|
44283
|
+
useVideo
|
|
44284
|
+
} = getDependencies();
|
|
44285
|
+
const [id, kind] = IS_URL$1.test(location) ? [getYoutubeId(location), "YOUTUBE_ID"] : [location, "READABLE_ID"];
|
|
44286
|
+
// The result value conforms to the wonder-blocks-data `Result` type
|
|
44287
|
+
// which is used by our GraphQL framework.
|
|
44288
|
+
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'VideoKind'.
|
|
44289
|
+
const result = useVideo(id, kind);
|
|
44290
|
+
const {
|
|
44291
|
+
strings
|
|
44292
|
+
} = usePerseusI18n();
|
|
44293
|
+
switch (result.status) {
|
|
44294
|
+
case "loading":
|
|
44295
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.loading);
|
|
44296
|
+
case "success":
|
|
44297
|
+
{
|
|
44298
|
+
const video = result.data?.video;
|
|
44299
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
44300
|
+
style: styles$4.transcriptLink
|
|
44301
|
+
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.Text, null, video?.title), /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
|
|
44302
|
+
size: 10
|
|
44303
|
+
}), /*#__PURE__*/React__namespace.createElement(Link__default["default"], {
|
|
44304
|
+
href: "/transcript/" + (video?.contentId || "videoNotFound"),
|
|
44305
|
+
target: "_blank",
|
|
44306
|
+
className: "visited-no-recolor"
|
|
44307
|
+
}, strings.videoTranscript));
|
|
44220
44308
|
}
|
|
44221
|
-
|
|
44309
|
+
case "error":
|
|
44310
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
|
|
44311
|
+
case "aborted":
|
|
44312
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
|
|
44313
|
+
default:
|
|
44314
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
|
|
44222
44315
|
}
|
|
44223
|
-
return obj;
|
|
44224
44316
|
};
|
|
44225
|
-
|
|
44226
|
-
|
|
44227
|
-
|
|
44317
|
+
const styles$4 = aphrodite.StyleSheet.create({
|
|
44318
|
+
transcriptLink: {
|
|
44319
|
+
flexDirection: "row",
|
|
44320
|
+
width: "100%",
|
|
44321
|
+
justifyContent: "center"
|
|
44228
44322
|
}
|
|
44229
|
-
|
|
44230
|
-
};
|
|
44323
|
+
});
|
|
44231
44324
|
|
|
44232
|
-
|
|
44233
|
-
*
|
|
44234
|
-
* lens constructor thaws, freeze delegates to type's freeze
|
|
44325
|
+
/**
|
|
44326
|
+
* This is a video widget for embedding videos in articles.
|
|
44235
44327
|
*/
|
|
44328
|
+
// Current default is 720p, based on the typical videos we upload currently
|
|
44329
|
+
const DEFAULT_WIDTH = 1280;
|
|
44330
|
+
const DEFAULT_HEIGHT = 720;
|
|
44331
|
+
const KA_EMBED = "{host}/embed_video?slug={slug}" + "&internal_video_only=1";
|
|
44332
|
+
const IS_URL = /^https?:\/\//;
|
|
44333
|
+
const IS_KA_SITE = /(khanacademy\.org|localhost)/;
|
|
44334
|
+
const IS_VIMEO = /(vimeo\.com)/;
|
|
44236
44335
|
|
|
44237
|
-
//
|
|
44238
|
-
// find the implementation to use for a given object
|
|
44239
|
-
var dispatch = function (x) {
|
|
44240
|
-
if (Array.isArray(x)) {
|
|
44241
|
-
return arr;
|
|
44242
|
-
} else if (isObject(x)) {
|
|
44243
|
-
return obj;
|
|
44244
|
-
} else if (typeof x === "string") {
|
|
44245
|
-
return str;
|
|
44246
|
-
}
|
|
44247
|
-
};
|
|
44336
|
+
// exports has no 'transform'
|
|
44248
44337
|
|
|
44249
|
-
|
|
44250
|
-
|
|
44251
|
-
|
|
44252
|
-
|
|
44338
|
+
/**
|
|
44339
|
+
* Video renderer.
|
|
44340
|
+
*/
|
|
44341
|
+
class Video extends React__namespace.Component {
|
|
44342
|
+
static contextType = PerseusI18nContext;
|
|
44343
|
+
/**
|
|
44344
|
+
* This is the widget's grading function.
|
|
44345
|
+
* Points for videos are tallied by the embedded video itself, in the case
|
|
44346
|
+
* of Khan Academy videos.
|
|
44347
|
+
*/
|
|
44348
|
+
static validate(userInput, rubric) {
|
|
44349
|
+
return {
|
|
44350
|
+
type: "points",
|
|
44351
|
+
earned: 0,
|
|
44352
|
+
total: 0,
|
|
44353
|
+
message: null
|
|
44354
|
+
};
|
|
44253
44355
|
}
|
|
44254
|
-
|
|
44255
|
-
return
|
|
44356
|
+
getUserInput = () => {
|
|
44357
|
+
return null;
|
|
44358
|
+
};
|
|
44359
|
+
simpleValidate = rubric => {
|
|
44360
|
+
return Video.validate(null, rubric);
|
|
44361
|
+
};
|
|
44362
|
+
change = (() => {
|
|
44363
|
+
var _this = this;
|
|
44364
|
+
return function () {
|
|
44365
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
44366
|
+
args[_key] = arguments[_key];
|
|
44367
|
+
}
|
|
44368
|
+
// @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
|
|
44369
|
+
return change.apply(_this, args);
|
|
44370
|
+
};
|
|
44371
|
+
})();
|
|
44372
|
+
render() {
|
|
44373
|
+
const {
|
|
44374
|
+
InitialRequestUrl
|
|
44375
|
+
} = getDependencies();
|
|
44376
|
+
const location = this.props.location;
|
|
44377
|
+
if (!location) {
|
|
44378
|
+
return /*#__PURE__*/React__namespace.createElement("div", null);
|
|
44379
|
+
}
|
|
44380
|
+
let url;
|
|
44381
|
+
if (IS_URL.test(location)) {
|
|
44382
|
+
url = location;
|
|
44383
|
+
if (IS_VIMEO.test(url)) {
|
|
44384
|
+
// If this is a vimeo video, we need to add the query string
|
|
44385
|
+
// parameter "dnt" so that analytics/tracking cookies aren't set.
|
|
44386
|
+
// https://help.vimeo.com/hc/en-us/articles/12426260232977-Player-parameters-overview
|
|
44387
|
+
if (url.indexOf("?") === -1) {
|
|
44388
|
+
url += "?dnt=1";
|
|
44389
|
+
} else {
|
|
44390
|
+
url += "&dnt=1";
|
|
44391
|
+
}
|
|
44392
|
+
}
|
|
44393
|
+
} else {
|
|
44394
|
+
url = KA_EMBED.replace("{slug}", location);
|
|
44395
|
+
let embedHostname = "https://www.khanacademy.org";
|
|
44396
|
+
if (IS_KA_SITE.test(InitialRequestUrl.host)) {
|
|
44397
|
+
embedHostname = InitialRequestUrl.origin;
|
|
44398
|
+
}
|
|
44399
|
+
url = url.replace("{host}", embedHostname);
|
|
44400
|
+
}
|
|
44401
|
+
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(FixedToResponsive, {
|
|
44402
|
+
width: DEFAULT_WIDTH,
|
|
44403
|
+
height: DEFAULT_HEIGHT
|
|
44404
|
+
// The key is here for the benefit of the editor, to ensure that
|
|
44405
|
+
// any changes cause a re-rendering of the frame.
|
|
44406
|
+
,
|
|
44407
|
+
key: location + this.props.alignment
|
|
44408
|
+
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
44409
|
+
style: a11y.srOnly
|
|
44410
|
+
}, this.context.strings.videoWrapper), /*#__PURE__*/React__namespace.createElement("iframe", {
|
|
44411
|
+
// TODO(joshuan): Consider not using iframes when we're
|
|
44412
|
+
// loading this from webapp. This iframe is problematic
|
|
44413
|
+
// for screenreaders.
|
|
44414
|
+
className: "perseus-video-widget",
|
|
44415
|
+
sandbox: "allow-same-origin allow-scripts",
|
|
44416
|
+
width: DEFAULT_WIDTH,
|
|
44417
|
+
height: DEFAULT_HEIGHT,
|
|
44418
|
+
src: url,
|
|
44419
|
+
allowFullScreen: true
|
|
44420
|
+
})), /*#__PURE__*/React__namespace.createElement(VideoTranscriptLink, {
|
|
44421
|
+
location: location
|
|
44422
|
+
}));
|
|
44256
44423
|
}
|
|
44257
|
-
|
|
44258
|
-
|
|
44259
|
-
|
|
44260
|
-
|
|
44261
|
-
|
|
44262
|
-
|
|
44263
|
-
|
|
44424
|
+
}
|
|
44425
|
+
var Video$1 = {
|
|
44426
|
+
name: "video",
|
|
44427
|
+
displayName: "Video",
|
|
44428
|
+
defaultAlignment: "block",
|
|
44429
|
+
supportedAlignments: ["block", "float-left", "float-right", "full-width"],
|
|
44430
|
+
widget: Video
|
|
44264
44431
|
};
|
|
44265
|
-
lens.prototype.zoom = function (lensArr) {
|
|
44266
|
-
if (this._zoomStack === undefined) {
|
|
44267
|
-
this._zoomStack = [];
|
|
44268
|
-
}
|
|
44269
|
-
this._zoomStack.push({
|
|
44270
|
-
zoom: lensArr,
|
|
44271
|
-
wrapped: this._wrapped
|
|
44272
|
-
});
|
|
44273
|
-
this._wrapped = lens(this._wrapped).get(lensArr);
|
|
44274
|
-
return this;
|
|
44275
|
-
};
|
|
44276
|
-
lens.prototype.deZoom = function () {
|
|
44277
|
-
var frame = this._zoomStack.pop();
|
|
44278
|
-
this._wrapped = lens(frame.wrapped).set(frame.zoom, this._wrapped).freeze();
|
|
44279
|
-
return this;
|
|
44280
|
-
};
|
|
44281
|
-
lens.prototype.get = function (lensArr) {
|
|
44282
|
-
var obj = this._wrapped;
|
|
44283
|
-
for (var i = 0; i < lensArr.length; i++) {
|
|
44284
|
-
obj = dispatch(obj).get(obj, lensArr[i]);
|
|
44285
|
-
}
|
|
44286
|
-
return obj;
|
|
44287
|
-
};
|
|
44288
|
-
lens.prototype.mod = function (lensArr, f) {
|
|
44289
|
-
var obj = this._wrapped;
|
|
44290
|
-
var newObj = clone(obj);
|
|
44291
|
-
var ops = dispatch(obj);
|
|
44292
|
-
if (lensArr.length === 0) {
|
|
44293
|
-
this._wrapped = f(this._wrapped);
|
|
44294
|
-
} else if (lensArr.length === 1) {
|
|
44295
|
-
this._wrapped = ops.mod(newObj, lensArr[0], f);
|
|
44296
|
-
} else {
|
|
44297
|
-
var monocle = lensArr[0];
|
|
44298
|
-
var shortLens = lensArr.slice(1);
|
|
44299
|
-
|
|
44300
|
-
// newObj = ops.mod(obj[monocle], shortLens, f);
|
|
44301
44432
|
|
|
44302
|
-
|
|
44303
|
-
|
|
44304
|
-
}
|
|
44305
|
-
return this;
|
|
44306
|
-
};
|
|
44433
|
+
/* eslint-disable import/no-named-as-default */
|
|
44434
|
+
var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, PythonProgram$1, Sorter$1, Table$1, Video$1, DeprecatedStandin$1];
|
|
44307
44435
|
|
|
44308
|
-
|
|
44309
|
-
|
|
44310
|
-
|
|
44311
|
-
|
|
44312
|
-
|
|
44313
|
-
|
|
44314
|
-
|
|
44436
|
+
/**
|
|
44437
|
+
* This should be called by all clients, specifying whether extra widgets are
|
|
44438
|
+
* needed via `loadExtraWidgets`. It is idempotent, so it's not a problem to
|
|
44439
|
+
* call it multiple times.
|
|
44440
|
+
*
|
|
44441
|
+
* skipMathJax:
|
|
44442
|
+
* If false/undefined, MathJax will be configured, and the
|
|
44443
|
+
* promise will wait for MathJax to load (if it hasn't already).
|
|
44444
|
+
*/
|
|
44445
|
+
const init = function (options) {
|
|
44446
|
+
registerWidgets(basicWidgets);
|
|
44447
|
+
registerWidgets(extraWidgets);
|
|
44448
|
+
replaceDeprecatedWidgets();
|
|
44315
44449
|
|
|
44316
|
-
//
|
|
44317
|
-
|
|
44318
|
-
|
|
44319
|
-
|
|
44320
|
-
|
|
44321
|
-
this._wrapped = ops.del(obj, lensArr[0]);
|
|
44322
|
-
} else {
|
|
44323
|
-
var monocle = lensArr[0];
|
|
44324
|
-
var shortLens = lensArr.slice(1);
|
|
44325
|
-
var newObj = clone(obj);
|
|
44326
|
-
newObj[monocle] = lens(obj[monocle]).del(shortLens).freeze();
|
|
44327
|
-
this._wrapped = newObj;
|
|
44450
|
+
// Pass skipMathJax: true if MathJax is already loaded and configured.
|
|
44451
|
+
const skipMathJax = options.skipMathJax;
|
|
44452
|
+
if (skipMathJax) {
|
|
44453
|
+
// @ts-expect-error - TS2322 - Type 'Promise<void>' is not assignable to type 'Promise<undefined>'.
|
|
44454
|
+
return Promise.resolve();
|
|
44328
44455
|
}
|
|
44329
|
-
return
|
|
44330
|
-
|
|
44331
|
-
|
|
44332
|
-
|
|
44333
|
-
|
|
44456
|
+
return new Promise((resolve, reject) => {
|
|
44457
|
+
MathJax.Hub.Config({
|
|
44458
|
+
messageStyle: "none",
|
|
44459
|
+
skipStartupTypeset: "none",
|
|
44460
|
+
"HTML-CSS": {
|
|
44461
|
+
availableFonts: ["TeX"],
|
|
44462
|
+
imageFont: null,
|
|
44463
|
+
scale: 100,
|
|
44464
|
+
showMathMenu: false
|
|
44465
|
+
}
|
|
44466
|
+
});
|
|
44467
|
+
MathJax.Hub.Configured();
|
|
44468
|
+
MathJax.Hub.Queue(resolve);
|
|
44334
44469
|
});
|
|
44335
44470
|
};
|
|
44336
44471
|
|
|
44337
|
-
//
|
|
44472
|
+
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
44473
|
+
const libName = "@khanacademy/perseus";
|
|
44474
|
+
const libVersion = "27.0.1";
|
|
44475
|
+
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
44338
44476
|
|
|
44339
|
-
const
|
|
44340
|
-
|
|
44341
|
-
|
|
44342
|
-
},
|
|
44343
|
-
set: function (arr, monocle, val) {
|
|
44344
|
-
var newArr = arr.splice();
|
|
44345
|
-
newArr[monocle] = val;
|
|
44346
|
-
return newArr;
|
|
44347
|
-
},
|
|
44348
|
-
mod: function (arr, monocle, f) {
|
|
44349
|
-
var newArr = arr.slice();
|
|
44350
|
-
newArr[monocle] = f(arr[monocle]);
|
|
44351
|
-
return newArr;
|
|
44352
|
-
},
|
|
44353
|
-
del: function (arr, monocle) {
|
|
44354
|
-
var newArr = arr.slice();
|
|
44355
|
-
newArr.splice(monocle, 1);
|
|
44356
|
-
return newArr;
|
|
44357
|
-
}
|
|
44477
|
+
const apiVersion = {
|
|
44478
|
+
major: 12,
|
|
44479
|
+
minor: 0
|
|
44358
44480
|
};
|
|
44359
|
-
|
|
44360
|
-
|
|
44361
|
-
|
|
44362
|
-
lens.prototype.insertAt = function(lensArr, toInsert) {
|
|
44363
|
-
var obj = this._wrapped;
|
|
44364
|
-
|
|
44365
|
-
var arrLens = lensArr.slice(0, -1);
|
|
44366
|
-
var arr = lens(obj).get(arrLens).slice(); // slice to copy
|
|
44367
|
-
|
|
44368
|
-
var arrIdx = lensArr[lensArr.length-1];
|
|
44369
|
-
arr.splice(arrIdx, 0, toInsert);
|
|
44370
|
-
return lens(obj).set(arrLens, arr);
|
|
44481
|
+
const itemDataVersion = {
|
|
44482
|
+
major: 0,
|
|
44483
|
+
minor: 1
|
|
44371
44484
|
};
|
|
44372
44485
|
|
|
44373
|
-
|
|
44374
|
-
lens.prototype.insertAfter = function(lensArr, toInsert) {
|
|
44375
|
-
var newLens = lensArr.slice();
|
|
44376
|
-
newLens[newLens.length-1] += 1;
|
|
44377
|
-
return lens(this._wrapped).insertAt(newLens, toInsert);
|
|
44378
|
-
};
|
|
44379
|
-
*/
|
|
44486
|
+
var allWidgets = [...basicWidgets, ...extraWidgets];
|
|
44380
44487
|
|
|
44381
|
-
|
|
44488
|
+
registerWidgets(allWidgets);
|
|
44489
|
+
const ItemVersion = getVersionVector();
|
|
44490
|
+
ItemVersion["::renderer::"] = itemDataVersion;
|
|
44382
44491
|
|
|
44383
|
-
|
|
44384
|
-
|
|
44385
|
-
|
|
44386
|
-
|
|
44387
|
-
|
|
44388
|
-
|
|
44389
|
-
|
|
44390
|
-
|
|
44391
|
-
}
|
|
44392
|
-
|
|
44393
|
-
|
|
44394
|
-
newObj[monocle] = f(obj[monocle]);
|
|
44395
|
-
return newObj;
|
|
44396
|
-
},
|
|
44397
|
-
del: function (obj, monocle) {
|
|
44398
|
-
var newObj = clone(obj);
|
|
44399
|
-
delete newObj[monocle];
|
|
44400
|
-
return newObj;
|
|
44492
|
+
class ArticleRenderer extends React__namespace.Component {
|
|
44493
|
+
static contextType = PerseusI18nContext;
|
|
44494
|
+
_currentFocus;
|
|
44495
|
+
sectionRenderers = [];
|
|
44496
|
+
static defaultProps = {
|
|
44497
|
+
apiOptions: ApiOptions.defaults,
|
|
44498
|
+
useNewStyles: false,
|
|
44499
|
+
linterContext: PerseusLinter__namespace.linterContextDefault
|
|
44500
|
+
};
|
|
44501
|
+
constructor(props) {
|
|
44502
|
+
super(props);
|
|
44401
44503
|
}
|
|
44402
|
-
|
|
44403
|
-
|
|
44404
|
-
// STRING FUNCTIONS
|
|
44405
|
-
|
|
44406
|
-
const str = {
|
|
44407
|
-
get: function (arr, monocle) {
|
|
44408
|
-
return arr[monocle];
|
|
44409
|
-
},
|
|
44410
|
-
set: function (arr, monocle, val) {
|
|
44411
|
-
var newArr = arr.splice();
|
|
44412
|
-
newArr[monocle] = val;
|
|
44413
|
-
return newArr;
|
|
44414
|
-
},
|
|
44415
|
-
mod: function (arr, monocle, f) {
|
|
44416
|
-
var newArr = arr.splice();
|
|
44417
|
-
newArr[monocle] = f(arr[monocle]);
|
|
44418
|
-
return newArr;
|
|
44419
|
-
},
|
|
44420
|
-
del: function (arr, monocle) {
|
|
44421
|
-
var newArr = arr.slice();
|
|
44422
|
-
newArr.splice(monocle);
|
|
44423
|
-
return newArr;
|
|
44504
|
+
componentDidMount() {
|
|
44505
|
+
this._currentFocus = null;
|
|
44424
44506
|
}
|
|
44425
|
-
|
|
44426
|
-
|
|
44427
|
-
/* eslint-disable eqeqeq, @typescript-eslint/no-unused-vars */
|
|
44428
|
-
/*
|
|
44429
|
-
* A class for formatting number to significant digits.
|
|
44430
|
-
* Copyright (C) 2001 Stephen Ostermiller
|
|
44431
|
-
* http://ostermiller.org/contact.pl?regarding?JavaScript+Significant+Figures
|
|
44432
|
-
*
|
|
44433
|
-
* This program is free software; you can redistribute it and/or modify
|
|
44434
|
-
* it under the terms of the GNU General Public License as published by
|
|
44435
|
-
* the Free Software Foundation; either version 2 of the License, or
|
|
44436
|
-
* (at your option) any later version.
|
|
44437
|
-
*
|
|
44438
|
-
* This program is distributed in the hope that it will be useful,
|
|
44439
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
44440
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
44441
|
-
* GNU General Public License for more details.
|
|
44442
|
-
*/
|
|
44443
|
-
|
|
44444
|
-
/**
|
|
44445
|
-
* An object tho both scans floating points to
|
|
44446
|
-
* determine the number of significant figures.
|
|
44447
|
-
* and can display a floating point using any number
|
|
44448
|
-
* of significant figures.
|
|
44449
|
-
*
|
|
44450
|
-
* @param s A string representation of a floating point.
|
|
44451
|
-
*/
|
|
44452
|
-
class SignificantFigures {
|
|
44453
|
-
order;
|
|
44454
|
-
mantissa;
|
|
44455
|
-
positive;
|
|
44456
|
-
sigFigs;
|
|
44457
|
-
sigDecs;
|
|
44458
|
-
constructor(s) {
|
|
44459
|
-
this.order = parseOrder(s);
|
|
44460
|
-
this.mantissa = parseMantissa(s);
|
|
44461
|
-
this.positive = parseSign(s);
|
|
44462
|
-
|
|
44463
|
-
/**
|
|
44464
|
-
* Get the number of significant figures this object uses.
|
|
44465
|
-
* Leading zeros are not significant. Traling zeros up to
|
|
44466
|
-
* and after the decimal point are significant.
|
|
44467
|
-
* Significant figures is good to know when the number is
|
|
44468
|
-
* used in multiplication.
|
|
44469
|
-
*
|
|
44470
|
-
* @return the number of significant figures.
|
|
44471
|
-
*/
|
|
44472
|
-
this.sigFigs = this.mantissa.length;
|
|
44473
|
-
|
|
44474
|
-
/**
|
|
44475
|
-
* Get the least significant decimal this object uses.
|
|
44476
|
-
* This is useful to know if a number is being used
|
|
44477
|
-
* in addition.
|
|
44478
|
-
* 400 - 2 the hundreds place (10^2) is the least significant decimal.
|
|
44479
|
-
* 75 - 0 the ones place (10^0) is the least significant decimal.
|
|
44480
|
-
* .543 - -3 the 1/1000's place (10^-3) is the least significant decimal.
|
|
44481
|
-
*
|
|
44482
|
-
* @return an integer representing the least significant decimal place.
|
|
44483
|
-
*/
|
|
44484
|
-
this.sigDecs = this.order - this.mantissa.length;
|
|
44507
|
+
shouldComponentUpdate(nextProps) {
|
|
44508
|
+
return nextProps !== this.props;
|
|
44485
44509
|
}
|
|
44486
|
-
|
|
44487
|
-
|
|
44488
|
-
|
|
44489
|
-
|
|
44490
|
-
|
|
44491
|
-
|
|
44492
|
-
|
|
44493
|
-
* figures, or the number of significant figures would be
|
|
44494
|
-
* ambiguous in the output if scientific notation were not
|
|
44495
|
-
* used.
|
|
44496
|
-
*
|
|
44497
|
-
* @param f A floating point number that should be displayed
|
|
44498
|
-
* @param sigFigs desired number of significant figures (integer).
|
|
44499
|
-
* @param sigDecs the least significant decimal place (integer).
|
|
44500
|
-
* @param scientific true iff scientific notation should always be used.
|
|
44501
|
-
* @return a string of this object formatted correctly.
|
|
44502
|
-
*/
|
|
44503
|
-
function displaySigFigs(f, sigFigs, sigDecs, scientific) {
|
|
44504
|
-
const s = "" + f;
|
|
44505
|
-
let order = parseOrder(s);
|
|
44506
|
-
let mantissa = parseMantissa(s);
|
|
44507
|
-
let positive = parseSign(s);
|
|
44508
|
-
let add;
|
|
44509
|
-
let decAdd;
|
|
44510
|
-
let sigAdd;
|
|
44511
|
-
let zeroScientific = false;
|
|
44512
|
-
if (f == 0 || mantissa == "" || mantissa == "0") {
|
|
44513
|
-
mantissa = "";
|
|
44514
|
-
for (let i = 0; i < sigFigs; i++) {
|
|
44515
|
-
mantissa += "0";
|
|
44510
|
+
_handleFocusChange = (newFocusPath, oldFocusPath) => {
|
|
44511
|
+
// TODO(charlie): DRY this up--some of this logic is repeated in
|
|
44512
|
+
// ItemRenderer.
|
|
44513
|
+
if (newFocusPath) {
|
|
44514
|
+
this._setCurrentFocus(newFocusPath);
|
|
44515
|
+
} else {
|
|
44516
|
+
this._onRendererBlur(oldFocusPath);
|
|
44516
44517
|
}
|
|
44517
|
-
|
|
44518
|
-
|
|
44519
|
-
|
|
44520
|
-
|
|
44521
|
-
|
|
44522
|
-
|
|
44523
|
-
|
|
44524
|
-
|
|
44525
|
-
|
|
44526
|
-
|
|
44527
|
-
|
|
44528
|
-
|
|
44529
|
-
|
|
44530
|
-
|
|
44531
|
-
|
|
44532
|
-
|
|
44533
|
-
|
|
44534
|
-
|
|
44535
|
-
|
|
44536
|
-
|
|
44537
|
-
|
|
44538
|
-
|
|
44539
|
-
|
|
44540
|
-
|
|
44541
|
-
positive = true;
|
|
44542
|
-
order = 1 + sigDecs;
|
|
44543
|
-
if (order != 0) {
|
|
44544
|
-
zeroScientific = true;
|
|
44545
|
-
}
|
|
44546
|
-
}
|
|
44547
|
-
}
|
|
44548
|
-
const useScientific = scientific || mantissa.length > 20 || order > 4 || order < -2 || order - mantissa.length > 0 && trailingZeros(mantissa) > 0 || zeroScientific;
|
|
44549
|
-
let returnVal = "";
|
|
44550
|
-
if (!positive) {
|
|
44551
|
-
returnVal += "-";
|
|
44552
|
-
}
|
|
44553
|
-
if (useScientific) {
|
|
44554
|
-
returnVal += mantissa.charAt(0);
|
|
44555
|
-
if (mantissa.length > 1) {
|
|
44556
|
-
returnVal += "." + mantissa.substring(1, mantissa.length);
|
|
44518
|
+
};
|
|
44519
|
+
_setCurrentFocus = newFocusPath => {
|
|
44520
|
+
const {
|
|
44521
|
+
keypadElement,
|
|
44522
|
+
apiOptions
|
|
44523
|
+
} = this.props;
|
|
44524
|
+
const {
|
|
44525
|
+
isMobile
|
|
44526
|
+
} = apiOptions;
|
|
44527
|
+
const prevFocusPath = this._currentFocus;
|
|
44528
|
+
this._currentFocus = newFocusPath;
|
|
44529
|
+
|
|
44530
|
+
// Use the section prefix to extract the relevant Renderer's input
|
|
44531
|
+
// paths, so as to check whether the focused path represents an
|
|
44532
|
+
// input.
|
|
44533
|
+
let didFocusInput = false;
|
|
44534
|
+
let focusedInput;
|
|
44535
|
+
if (this._currentFocus) {
|
|
44536
|
+
const [sectionIndex, ...focusPath] = this._currentFocus;
|
|
44537
|
+
const inputPaths = this.sectionRenderers[sectionIndex].getInputPaths();
|
|
44538
|
+
didFocusInput = inputPaths.some(inputPath => {
|
|
44539
|
+
return Util.inputPathsEqual(inputPath, focusPath);
|
|
44540
|
+
});
|
|
44541
|
+
focusedInput = this.sectionRenderers[sectionIndex].getDOMNodeForPath(focusPath);
|
|
44557
44542
|
}
|
|
44558
|
-
|
|
44559
|
-
|
|
44543
|
+
const {
|
|
44544
|
+
onFocusChange
|
|
44545
|
+
} = this.props.apiOptions;
|
|
44546
|
+
if (onFocusChange) {
|
|
44547
|
+
// Wait for the keypad to mount before getting the height
|
|
44548
|
+
setTimeout(() => {
|
|
44549
|
+
const keypadDomNode = keypadElement?.getDOMNode();
|
|
44550
|
+
const keypadHeight = keypadDomNode && didFocusInput ? keypadDomNode.getBoundingClientRect().height : 0;
|
|
44551
|
+
onFocusChange(this._currentFocus, prevFocusPath, keypadHeight, didFocusInput ? focusedInput : null);
|
|
44552
|
+
}, 0);
|
|
44560
44553
|
}
|
|
44561
|
-
|
|
44562
|
-
|
|
44563
|
-
|
|
44564
|
-
let needDot = true;
|
|
44565
|
-
if (order > 0) {
|
|
44566
|
-
if (mantissa.length > order) {
|
|
44567
|
-
wholePart = mantissa.substring(0, order);
|
|
44568
|
-
fractPart = mantissa.substring(order, mantissa.length);
|
|
44554
|
+
if (keypadElement && isMobile) {
|
|
44555
|
+
if (didFocusInput) {
|
|
44556
|
+
keypadElement.activate();
|
|
44569
44557
|
} else {
|
|
44570
|
-
|
|
44571
|
-
needDot = trailingZeros(mantissa) != 0;
|
|
44572
|
-
for (let i = 0; i < order - mantissa.length; i++) {
|
|
44573
|
-
wholePart += "0";
|
|
44574
|
-
}
|
|
44575
|
-
}
|
|
44576
|
-
} else {
|
|
44577
|
-
for (let i = 0; i < -order; i++) {
|
|
44578
|
-
fractPart += "0";
|
|
44558
|
+
keypadElement.dismiss();
|
|
44579
44559
|
}
|
|
44580
|
-
fractPart += mantissa;
|
|
44581
44560
|
}
|
|
44582
|
-
|
|
44583
|
-
|
|
44584
|
-
|
|
44585
|
-
}
|
|
44561
|
+
};
|
|
44562
|
+
_onRendererBlur = blurPath => {
|
|
44563
|
+
const blurringFocusPath = this._currentFocus;
|
|
44586
44564
|
|
|
44587
|
-
|
|
44588
|
-
|
|
44589
|
-
|
|
44590
|
-
|
|
44591
|
-
*/
|
|
44592
|
-
function trailingZeros(mantissa) {
|
|
44593
|
-
let zeros = 0;
|
|
44594
|
-
for (let i = mantissa.length - 1; i >= 0; i--) {
|
|
44595
|
-
const c = mantissa.charAt(i);
|
|
44596
|
-
if (c == "0") {
|
|
44597
|
-
zeros++;
|
|
44598
|
-
} else {
|
|
44599
|
-
return zeros;
|
|
44565
|
+
// Failsafe: abort if ID is different, because focus probably happened
|
|
44566
|
+
// before blur.
|
|
44567
|
+
if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
|
|
44568
|
+
return;
|
|
44600
44569
|
}
|
|
44601
|
-
}
|
|
44602
|
-
return zeros;
|
|
44603
|
-
}
|
|
44604
44570
|
|
|
44605
|
-
|
|
44606
|
-
|
|
44607
|
-
|
|
44608
|
-
|
|
44609
|
-
|
|
44610
|
-
|
|
44611
|
-
|
|
44612
|
-
|
|
44613
|
-
let beginning = true;
|
|
44614
|
-
let seenDot = false;
|
|
44615
|
-
let seenSomething = false;
|
|
44616
|
-
let zeros = "";
|
|
44617
|
-
let all = "";
|
|
44618
|
-
let pos = true;
|
|
44619
|
-
for (let i = 0; i < s.length; i++) {
|
|
44620
|
-
const c = s.charAt(i);
|
|
44621
|
-
if (c >= "1" && c <= "9") {
|
|
44622
|
-
all += zeros + c;
|
|
44623
|
-
zeros = "";
|
|
44624
|
-
seenSomething = true;
|
|
44625
|
-
beginning = false;
|
|
44626
|
-
} else if (c == "0") {
|
|
44627
|
-
if (seenDot) {
|
|
44628
|
-
if (seenSomething) {
|
|
44629
|
-
all += zeros + c;
|
|
44630
|
-
zeros = "";
|
|
44631
|
-
}
|
|
44632
|
-
} else {
|
|
44633
|
-
if (seenSomething) {
|
|
44634
|
-
zeros += c;
|
|
44635
|
-
}
|
|
44571
|
+
// Wait until after any new focus events fire this tick before declaring
|
|
44572
|
+
// that nothing is focused, since if there were a focus change across
|
|
44573
|
+
// sections, we could receive the blur before the focus.
|
|
44574
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
44575
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
44576
|
+
setTimeout(() => {
|
|
44577
|
+
if (Util.inputPathsEqual(this._currentFocus, blurringFocusPath)) {
|
|
44578
|
+
this._setCurrentFocus(null);
|
|
44636
44579
|
}
|
|
44637
|
-
|
|
44638
|
-
|
|
44639
|
-
|
|
44640
|
-
|
|
44641
|
-
|
|
44642
|
-
|
|
44643
|
-
}
|
|
44644
|
-
|
|
44645
|
-
|
|
44646
|
-
|
|
44647
|
-
|
|
44648
|
-
|
|
44580
|
+
});
|
|
44581
|
+
};
|
|
44582
|
+
blur = () => {
|
|
44583
|
+
if (this._currentFocus) {
|
|
44584
|
+
const [sectionIndex, ...inputPath] = this._currentFocus;
|
|
44585
|
+
this.sectionRenderers[sectionIndex].blurPath(inputPath);
|
|
44586
|
+
}
|
|
44587
|
+
};
|
|
44588
|
+
_sections = () => {
|
|
44589
|
+
const sections = Array.isArray(this.props.json) ? this.props.json : [this.props.json];
|
|
44590
|
+
|
|
44591
|
+
// In JIPT context we split sections to paragraphs in order match the
|
|
44592
|
+
// translatable strings found on Crowdin when rendering articles in
|
|
44593
|
+
// the WYSIWYG mode for translation. This is needed for the jipt.js
|
|
44594
|
+
// integration in order to attribute the rendered strings on Crowdin.
|
|
44595
|
+
if (getDependencies().JIPT.useJIPT) {
|
|
44596
|
+
const paragraphs = [];
|
|
44597
|
+
for (const section of sections) {
|
|
44598
|
+
JiptParagraphs.parseToArray(section.content).forEach(paragraph => {
|
|
44599
|
+
paragraphs.push({
|
|
44600
|
+
...section,
|
|
44601
|
+
content: paragraph
|
|
44602
|
+
});
|
|
44603
|
+
});
|
|
44649
44604
|
}
|
|
44605
|
+
return paragraphs;
|
|
44650
44606
|
}
|
|
44651
|
-
|
|
44652
|
-
|
|
44653
|
-
|
|
44654
|
-
|
|
44655
|
-
|
|
44656
|
-
|
|
44657
|
-
|
|
44658
|
-
|
|
44659
|
-
|
|
44660
|
-
|
|
44661
|
-
|
|
44662
|
-
|
|
44663
|
-
|
|
44664
|
-
|
|
44665
|
-
|
|
44666
|
-
|
|
44667
|
-
|
|
44668
|
-
|
|
44669
|
-
|
|
44670
|
-
|
|
44671
|
-
|
|
44672
|
-
|
|
44673
|
-
|
|
44674
|
-
|
|
44675
|
-
|
|
44676
|
-
|
|
44677
|
-
|
|
44678
|
-
|
|
44679
|
-
|
|
44680
|
-
|
|
44681
|
-
|
|
44682
|
-
|
|
44683
|
-
|
|
44684
|
-
|
|
44685
|
-
|
|
44686
|
-
|
|
44687
|
-
|
|
44688
|
-
|
|
44689
|
-
|
|
44690
|
-
|
|
44691
|
-
|
|
44692
|
-
|
|
44693
|
-
|
|
44694
|
-
|
|
44695
|
-
|
|
44696
|
-
|
|
44697
|
-
}
|
|
44698
|
-
|
|
44699
|
-
|
|
44700
|
-
}
|
|
44701
|
-
|
|
44702
|
-
|
|
44703
|
-
return leadZeros;
|
|
44704
|
-
}
|
|
44705
|
-
return all;
|
|
44706
|
-
}
|
|
44707
|
-
|
|
44708
|
-
/**
|
|
44709
|
-
* Parse a string representation of a floating point
|
|
44710
|
-
* and pull out the exponent.
|
|
44711
|
-
*
|
|
44712
|
-
* @param s the string representation of a floating point.
|
|
44713
|
-
* @return (integer) the number after the e.
|
|
44714
|
-
*/
|
|
44715
|
-
function parseOrder(s) {
|
|
44716
|
-
let seenDot = false;
|
|
44717
|
-
let seenSomething = false;
|
|
44718
|
-
let zeros = "";
|
|
44719
|
-
let all = "";
|
|
44720
|
-
let decPlaces = 0;
|
|
44721
|
-
let totalDecs = 0;
|
|
44722
|
-
for (let i = 0; i < s.length; i++) {
|
|
44723
|
-
const c = s.charAt(i);
|
|
44724
|
-
if (c >= "1" && c <= "9") {
|
|
44725
|
-
all += zeros + c;
|
|
44726
|
-
zeros = "";
|
|
44727
|
-
seenSomething = true;
|
|
44728
|
-
if (!seenDot) {
|
|
44729
|
-
totalDecs++;
|
|
44730
|
-
decPlaces++;
|
|
44731
|
-
}
|
|
44732
|
-
} else if (c == "0") {
|
|
44733
|
-
if (seenDot) {
|
|
44734
|
-
if (seenSomething) {
|
|
44735
|
-
all += zeros + c;
|
|
44736
|
-
zeros = "";
|
|
44737
|
-
} else {
|
|
44738
|
-
decPlaces--;
|
|
44739
|
-
}
|
|
44740
|
-
} else {
|
|
44741
|
-
totalDecs++;
|
|
44742
|
-
if (seenSomething) {
|
|
44743
|
-
decPlaces++;
|
|
44744
|
-
zeros += c;
|
|
44745
|
-
}
|
|
44746
|
-
}
|
|
44747
|
-
} else if (!seenDot && c == ".") {
|
|
44748
|
-
all += zeros;
|
|
44749
|
-
zeros = "";
|
|
44750
|
-
seenDot = true;
|
|
44751
|
-
} else if (c == "e" || c == "E" && i + 1 < s.length) {
|
|
44752
|
-
const raised = parseInt(s.substring(i + 1, s.length));
|
|
44753
|
-
decPlaces += raised;
|
|
44754
|
-
totalDecs += raised;
|
|
44755
|
-
i = s.length;
|
|
44756
|
-
} else ;
|
|
44757
|
-
}
|
|
44758
|
-
if (all == "") {
|
|
44759
|
-
return totalDecs;
|
|
44760
|
-
}
|
|
44761
|
-
return decPlaces;
|
|
44762
|
-
}
|
|
44763
|
-
|
|
44764
|
-
/**
|
|
44765
|
-
* Remove the specified number of digits from string by
|
|
44766
|
-
* rounding. Proper rounding rules for scientific purposes
|
|
44767
|
-
* are followed.
|
|
44768
|
-
* This method may cause an extra significant figure
|
|
44769
|
-
* to be added to the number. For example, if 999999
|
|
44770
|
-
* were rounded, A one would carry over and become
|
|
44771
|
-
* a significant figure. Those who call this method
|
|
44772
|
-
* should check for this and call round again if needed.
|
|
44773
|
-
*
|
|
44774
|
-
* @param mantissa A string representing an whole number of arbitrary length.
|
|
44775
|
-
* @param digits A number of digits to remove
|
|
44776
|
-
* @return A string represted the rounded version of mantissa
|
|
44777
|
-
*/
|
|
44778
|
-
function round(mantissa, digits) {
|
|
44779
|
-
const last = mantissa.length - digits - 1;
|
|
44780
|
-
if (last < 0) {
|
|
44781
|
-
return "";
|
|
44782
|
-
}
|
|
44783
|
-
if (last >= mantissa.length - 1) {
|
|
44784
|
-
return mantissa;
|
|
44785
|
-
}
|
|
44786
|
-
const nextToLast = mantissa.charAt(last + 1);
|
|
44787
|
-
const lastChar = mantissa.charAt(last);
|
|
44788
|
-
let roundUp = false;
|
|
44789
|
-
if (nextToLast > "5") {
|
|
44790
|
-
roundUp = true;
|
|
44791
|
-
} else if (nextToLast == "5") {
|
|
44792
|
-
for (let j = last + 2; j < mantissa.length; j++) {
|
|
44793
|
-
if (mantissa.charAt(j) != "0") {
|
|
44794
|
-
roundUp = true;
|
|
44795
|
-
}
|
|
44796
|
-
}
|
|
44797
|
-
// @ts-expect-error - TS2362 - The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
|
|
44798
|
-
if (lastChar % 2 == 1) {
|
|
44799
|
-
roundUp = true;
|
|
44800
|
-
}
|
|
44801
|
-
}
|
|
44802
|
-
let result = "";
|
|
44803
|
-
for (let i = last; i >= 0; i--) {
|
|
44804
|
-
const c = mantissa.charAt(i);
|
|
44805
|
-
if (roundUp) {
|
|
44806
|
-
let nextChar;
|
|
44807
|
-
if (c == "9") {
|
|
44808
|
-
nextChar = "0";
|
|
44809
|
-
} else {
|
|
44810
|
-
switch (c) {
|
|
44811
|
-
case "0":
|
|
44812
|
-
nextChar = "1";
|
|
44813
|
-
break;
|
|
44814
|
-
case "1":
|
|
44815
|
-
nextChar = "2";
|
|
44816
|
-
break;
|
|
44817
|
-
case "2":
|
|
44818
|
-
nextChar = "3";
|
|
44819
|
-
break;
|
|
44820
|
-
case "3":
|
|
44821
|
-
nextChar = "4";
|
|
44822
|
-
break;
|
|
44823
|
-
case "4":
|
|
44824
|
-
nextChar = "5";
|
|
44825
|
-
break;
|
|
44826
|
-
case "5":
|
|
44827
|
-
nextChar = "6";
|
|
44828
|
-
break;
|
|
44829
|
-
case "6":
|
|
44830
|
-
nextChar = "7";
|
|
44831
|
-
break;
|
|
44832
|
-
case "7":
|
|
44833
|
-
nextChar = "8";
|
|
44834
|
-
break;
|
|
44835
|
-
case "8":
|
|
44836
|
-
nextChar = "9";
|
|
44837
|
-
break;
|
|
44838
|
-
}
|
|
44839
|
-
roundUp = false;
|
|
44840
|
-
}
|
|
44841
|
-
result = nextChar + result;
|
|
44842
|
-
} else {
|
|
44843
|
-
result = c + result;
|
|
44844
|
-
}
|
|
44845
|
-
}
|
|
44846
|
-
if (roundUp) {
|
|
44847
|
-
result = "1" + result;
|
|
44848
|
-
}
|
|
44849
|
-
return result;
|
|
44850
|
-
}
|
|
44851
|
-
|
|
44852
|
-
// TODO(joel): teach KAS how to accept an answer only if it's expressed in
|
|
44853
|
-
const ALL = "all";
|
|
44854
|
-
const countSigfigs = function (value) {
|
|
44855
|
-
return new SignificantFigures(value).sigFigs;
|
|
44856
|
-
};
|
|
44857
|
-
/* I just wrote this, but it's old by analogy to `OldExpression`, in that it's
|
|
44858
|
-
* the version that non-mathquill platforms get stuck with. Constructed with an
|
|
44859
|
-
* <input>, a parser, popsicle sticks, and glue.
|
|
44860
|
-
*
|
|
44861
|
-
* In the same way as OldExpression, this parses continuously as you type, then
|
|
44862
|
-
* shows and hides an error buddy. The error message is only shown after a
|
|
44863
|
-
* rolling two second delay, but hidden immediately on further typing.
|
|
44864
|
-
*/
|
|
44865
|
-
class OldUnitInput extends React__namespace.Component {
|
|
44866
|
-
static contextType = PerseusI18nContext;
|
|
44867
|
-
_errorTimeout;
|
|
44868
|
-
static defaultProps = {
|
|
44869
|
-
apiOptions: ApiOptions.defaults,
|
|
44870
|
-
value: ""
|
|
44871
|
-
};
|
|
44872
|
-
static validate(userInput, rubric, strings) {
|
|
44873
|
-
const answer = KAS__namespace.unitParse(rubric.value).expr;
|
|
44874
|
-
const guess = KAS__namespace.unitParse(userInput);
|
|
44875
|
-
if (!guess.parsed) {
|
|
44876
|
-
return {
|
|
44877
|
-
type: "invalid",
|
|
44878
|
-
message: strings.dontUnderstandUnits
|
|
44879
|
-
};
|
|
44880
|
-
}
|
|
44881
|
-
|
|
44882
|
-
// Note: we check sigfigs, then numerical correctness, then units, so
|
|
44883
|
-
// the most significant things come last, that way the user will see
|
|
44884
|
-
// the most important message.
|
|
44885
|
-
let message = null;
|
|
44886
|
-
|
|
44887
|
-
// did the user specify the right number of sigfigs?
|
|
44888
|
-
// TODO(joel) - add a grading mode where the wrong number of sigfigs
|
|
44889
|
-
// isn't marked wrong
|
|
44890
|
-
const sigfigs = rubric.sigfigs;
|
|
44891
|
-
const sigfigsCorrect = countSigfigs(guess.coefficient) === sigfigs;
|
|
44892
|
-
if (!sigfigsCorrect) {
|
|
44893
|
-
message = strings.checkSigFigs;
|
|
44894
|
-
}
|
|
44895
|
-
|
|
44896
|
-
// now we need to check that the answer is correct to the precision we
|
|
44897
|
-
// require.
|
|
44898
|
-
let numericallyCorrect;
|
|
44899
|
-
try {
|
|
44900
|
-
const x = new KAS__namespace.Var("x");
|
|
44901
|
-
const equality = new KAS__namespace.Eq(answer.simplify(), "=", new KAS__namespace.Mul(x, guess.expr.simplify()));
|
|
44902
|
-
const conversion = equality.solveLinearEquationForVariable(x);
|
|
44903
|
-
|
|
44904
|
-
// Make sure the conversion factor between the user's input answer
|
|
44905
|
-
// and the canonical answer is 1, to sigfig places.
|
|
44906
|
-
// TODO(joel) is this sound?
|
|
44907
|
-
numericallyCorrect = Number(conversion.eval()).toPrecision(sigfigs) === Number(1).toPrecision(sigfigs);
|
|
44908
|
-
} catch (e) {
|
|
44909
|
-
numericallyCorrect = false;
|
|
44910
|
-
}
|
|
44911
|
-
if (!numericallyCorrect) {
|
|
44912
|
-
message = strings.answerNumericallyIncorrect;
|
|
44913
|
-
}
|
|
44914
|
-
let kasCorrect;
|
|
44915
|
-
const guessUnit = primUnits(guess.expr.simplify());
|
|
44916
|
-
const answerUnit = primUnits(answer.simplify());
|
|
44917
|
-
if (rubric.accepting === ALL) {
|
|
44918
|
-
// We're accepting all units - KAS does the hard work of figuring
|
|
44919
|
-
// out if the user's unit is equivalent to the author's unit.
|
|
44920
|
-
kasCorrect = KAS__namespace.compare(guessUnit, answerUnit).equal;
|
|
44921
|
-
} else {
|
|
44922
|
-
// Are any of the accepted units the same as what the user entered?
|
|
44923
|
-
kasCorrect = ___default["default"](rubric.acceptingUnits).any(unit => {
|
|
44924
|
-
const thisAnswerUnit = primUnits(KAS__namespace.unitParse(unit).unit.simplify());
|
|
44925
|
-
return KAS__namespace.compare(thisAnswerUnit, guessUnit
|
|
44926
|
-
// TODO(joel) - make this work as intended.
|
|
44927
|
-
// { form: true }
|
|
44928
|
-
).equal;
|
|
44929
|
-
});
|
|
44930
|
-
}
|
|
44931
|
-
if (!kasCorrect) {
|
|
44932
|
-
message = strings.checkUnits;
|
|
44933
|
-
}
|
|
44934
|
-
const correct = kasCorrect && numericallyCorrect && sigfigsCorrect;
|
|
44935
|
-
return {
|
|
44936
|
-
type: "points",
|
|
44937
|
-
earned: correct ? 1 : 0,
|
|
44938
|
-
total: 1,
|
|
44939
|
-
message
|
|
44940
|
-
};
|
|
44941
|
-
}
|
|
44942
|
-
componentDidUpdate() {
|
|
44943
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
44944
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
44945
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
44946
|
-
clearTimeout(this._errorTimeout);
|
|
44947
|
-
if (KAS__namespace.unitParse(this.props.value).parsed) {
|
|
44948
|
-
this._hideError();
|
|
44949
|
-
} else {
|
|
44950
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
44951
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
44952
|
-
// @ts-expect-error - TS2322 - Type 'Timeout' is not assignable to type 'number'.
|
|
44953
|
-
this._errorTimeout = setTimeout(this._showError, 2000);
|
|
44954
|
-
}
|
|
44955
|
-
}
|
|
44956
|
-
componentWillUnmount() {
|
|
44957
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
44958
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
44959
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
44960
|
-
clearTimeout(this._errorTimeout);
|
|
44961
|
-
}
|
|
44962
|
-
_showError = () => {
|
|
44963
|
-
if (this.props.value === "") {
|
|
44964
|
-
return;
|
|
44965
|
-
}
|
|
44966
|
-
|
|
44967
|
-
// eslint-disable-next-line react/no-string-refs
|
|
44968
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
44969
|
-
const $error = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.error));
|
|
44970
|
-
// @ts-expect-error - TS2339 - Property 'is' does not exist on type 'JQueryStatic'.
|
|
44971
|
-
if (!$error.is(":visible")) {
|
|
44972
|
-
$error
|
|
44973
|
-
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
44974
|
-
.css({
|
|
44975
|
-
top: 50,
|
|
44976
|
-
opacity: 0.1
|
|
44977
|
-
}).show().animate({
|
|
44978
|
-
top: 0,
|
|
44979
|
-
opacity: 1.0
|
|
44980
|
-
}, 300);
|
|
44981
|
-
}
|
|
44982
|
-
};
|
|
44983
|
-
_hideError = () => {
|
|
44984
|
-
// eslint-disable-next-line react/no-string-refs
|
|
44985
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
44986
|
-
const $error = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.error));
|
|
44987
|
-
// @ts-expect-error - TS2339 - Property 'is' does not exist on type 'JQueryStatic'.
|
|
44988
|
-
if ($error.is(":visible")) {
|
|
44989
|
-
// @ts-expect-error - TS2339 - Property 'animate' does not exist on type 'JQueryStatic'.
|
|
44990
|
-
$error.animate({
|
|
44991
|
-
top: 50,
|
|
44992
|
-
opacity: 0.1
|
|
44993
|
-
}, 300, function () {
|
|
44994
|
-
// eslint-disable-next-line @babel/no-invalid-this
|
|
44995
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
44996
|
-
// eslint-disable-next-line @babel/no-invalid-this
|
|
44997
|
-
$__default["default"](this).hide();
|
|
44998
|
-
});
|
|
44999
|
-
}
|
|
45000
|
-
};
|
|
45001
|
-
change = (() => {
|
|
45002
|
-
var _this = this;
|
|
45003
|
-
return function () {
|
|
45004
|
-
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
45005
|
-
args[_key] = arguments[_key];
|
|
45006
|
-
}
|
|
45007
|
-
// @ts-expect-error - TS2345 - Argument of type 'readonly any[]' is not assignable to parameter of type 'any[]'.
|
|
45008
|
-
return change.apply(_this, args);
|
|
45009
|
-
};
|
|
45010
|
-
})();
|
|
45011
|
-
handleBlur = () => {
|
|
45012
|
-
this.props.onBlur([]);
|
|
45013
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
45014
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
45015
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
45016
|
-
clearTimeout(this._errorTimeout);
|
|
45017
|
-
if (!KAS__namespace.unitParse(this.props.value).parsed) {
|
|
45018
|
-
this._showError();
|
|
45019
|
-
}
|
|
45020
|
-
};
|
|
45021
|
-
handleChange = event => {
|
|
45022
|
-
this._hideError();
|
|
45023
|
-
this.props.onChange({
|
|
45024
|
-
value: event.target.value
|
|
45025
|
-
});
|
|
45026
|
-
};
|
|
45027
|
-
simpleValidate = (rubric, onInputError) => {
|
|
45028
|
-
return OldUnitInput.validate(this.getUserInput(), rubric, this.context.strings);
|
|
45029
|
-
};
|
|
45030
|
-
getUserInput = () => {
|
|
45031
|
-
return this.props.value;
|
|
45032
|
-
};
|
|
45033
|
-
|
|
45034
|
-
// begin mobile stuff
|
|
45035
|
-
|
|
45036
|
-
getInputPaths = () => {
|
|
45037
|
-
// The widget itself is an input, so we return a single empty list to
|
|
45038
|
-
// indicate this.
|
|
45039
|
-
return [[]];
|
|
45040
|
-
};
|
|
45041
|
-
focusInputPath = inputPath => {
|
|
45042
|
-
// eslint-disable-next-line react/no-string-refs
|
|
45043
|
-
const input = ReactDOM__default["default"].findDOMNode(this.refs.input);
|
|
45044
|
-
if (input) {
|
|
45045
|
-
/**
|
|
45046
|
-
* TODO(somewhatabstract, JIRA-XXXX):
|
|
45047
|
-
* Change to using a ref callback so that focus() can be
|
|
45048
|
-
* accessed.
|
|
45049
|
-
*/
|
|
45050
|
-
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
|
|
45051
|
-
input.focus();
|
|
45052
|
-
}
|
|
45053
|
-
};
|
|
45054
|
-
handleFocus = () => {
|
|
45055
|
-
this.props.onFocus([]);
|
|
45056
|
-
};
|
|
45057
|
-
blurInputPath = inputPath => {
|
|
45058
|
-
// eslint-disable-next-line react/no-string-refs
|
|
45059
|
-
const input = ReactDOM__default["default"].findDOMNode(this.refs.input);
|
|
45060
|
-
if (input) {
|
|
45061
|
-
/**
|
|
45062
|
-
* TODO(somewhatabstract, JIRA-XXXX):
|
|
45063
|
-
* Change to using a ref callback so that focus() can be
|
|
45064
|
-
* accessed.
|
|
45065
|
-
*/
|
|
45066
|
-
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'Element | Text'.
|
|
45067
|
-
input.blur();
|
|
45068
|
-
}
|
|
45069
|
-
};
|
|
45070
|
-
setInputValue = (path, newValue, cb) => {
|
|
45071
|
-
this.props.onChange({
|
|
45072
|
-
value: newValue
|
|
45073
|
-
}, cb);
|
|
45074
|
-
};
|
|
45075
|
-
getDOMNodeForPath = () => {
|
|
45076
|
-
// eslint-disable-next-line react/no-string-refs
|
|
45077
|
-
return ReactDOM__default["default"].findDOMNode(this.refs.input);
|
|
45078
|
-
};
|
|
45079
|
-
getGrammarTypeForPath = inputPath => {
|
|
45080
|
-
return "unit";
|
|
45081
|
-
};
|
|
45082
|
-
|
|
45083
|
-
// end mobile stuff
|
|
45084
|
-
|
|
45085
|
-
// TODO(joel) think about showing the error buddy
|
|
45086
|
-
render() {
|
|
45087
|
-
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
45088
|
-
className: "old-unit-input"
|
|
45089
|
-
}, /*#__PURE__*/React__namespace.createElement("input", {
|
|
45090
|
-
onChange: this.handleChange,
|
|
45091
|
-
ref: "input" // eslint-disable-line react/no-string-refs
|
|
45092
|
-
,
|
|
45093
|
-
className: ClassNames.INTERACTIVE,
|
|
45094
|
-
value: this.props.value,
|
|
45095
|
-
onFocus: this.handleFocus,
|
|
45096
|
-
onBlur: this.handleBlur
|
|
45097
|
-
}), /*#__PURE__*/React__namespace.createElement("div", {
|
|
45098
|
-
ref: "error",
|
|
45099
|
-
className: "error",
|
|
45100
|
-
style: {
|
|
45101
|
-
display: "none"
|
|
45102
|
-
}
|
|
45103
|
-
}, this.context.strings.dontUnderstand));
|
|
45104
|
-
}
|
|
45105
|
-
}
|
|
45106
|
-
|
|
45107
|
-
// Extract the primitive units from a unit expression. This first simplifies
|
|
45108
|
-
// `expr` to a `Mul` like "5 kg m / s^2" then removes the first term.
|
|
45109
|
-
const primUnits = function (expr) {
|
|
45110
|
-
return expr.simplify().asMul().partition()[1].flatten().simplify();
|
|
45111
|
-
};
|
|
45112
|
-
var Unit = {
|
|
45113
|
-
name: "unit-input",
|
|
45114
|
-
displayName: "Unit input",
|
|
45115
|
-
defaultAlignment: "inline-block",
|
|
45116
|
-
widget: OldUnitInput,
|
|
45117
|
-
transform: x => lens(x).del(["value"]).freeze(),
|
|
45118
|
-
version: {
|
|
45119
|
-
major: 0,
|
|
45120
|
-
minor: 1
|
|
45121
|
-
},
|
|
45122
|
-
hidden: true
|
|
45123
|
-
};
|
|
45124
|
-
|
|
45125
|
-
/**
|
|
45126
|
-
* This is a component that adds a link to the transcript for embedded videos in articles.
|
|
45127
|
-
*/
|
|
45128
|
-
const IS_URL$1 = /^https?:\/\//;
|
|
45129
|
-
const getYoutubeId = url => {
|
|
45130
|
-
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
|
45131
|
-
const match = url.match(regExp);
|
|
45132
|
-
if (match && match[7].length === 11) {
|
|
45133
|
-
return match[7];
|
|
45134
|
-
}
|
|
45135
|
-
return "videoNotFound";
|
|
45136
|
-
};
|
|
45137
|
-
|
|
45138
|
-
// will accept a youtube link or a video's slug name from embedded video and then query accordingly.
|
|
45139
|
-
|
|
45140
|
-
/**
|
|
45141
|
-
* Video Transcript Link Component.
|
|
45142
|
-
*/
|
|
45143
|
-
const VideoTranscriptLink = props => {
|
|
45144
|
-
const {
|
|
45145
|
-
location
|
|
45146
|
-
} = props;
|
|
45147
|
-
const {
|
|
45148
|
-
useVideo
|
|
45149
|
-
} = getDependencies();
|
|
45150
|
-
const [id, kind] = IS_URL$1.test(location) ? [getYoutubeId(location), "YOUTUBE_ID"] : [location, "READABLE_ID"];
|
|
45151
|
-
// The result value conforms to the wonder-blocks-data `Result` type
|
|
45152
|
-
// which is used by our GraphQL framework.
|
|
45153
|
-
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'VideoKind'.
|
|
45154
|
-
const result = useVideo(id, kind);
|
|
45155
|
-
const {
|
|
45156
|
-
strings
|
|
45157
|
-
} = usePerseusI18n();
|
|
45158
|
-
switch (result.status) {
|
|
45159
|
-
case "loading":
|
|
45160
|
-
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.loading);
|
|
45161
|
-
case "success":
|
|
45162
|
-
{
|
|
45163
|
-
const video = result.data?.video;
|
|
45164
|
-
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
45165
|
-
style: styles$4.transcriptLink
|
|
45166
|
-
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.Text, null, video?.title), /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
|
|
45167
|
-
size: 10
|
|
45168
|
-
}), /*#__PURE__*/React__namespace.createElement(Link__default["default"], {
|
|
45169
|
-
href: "/transcript/" + (video?.contentId || "videoNotFound"),
|
|
45170
|
-
target: "_blank",
|
|
45171
|
-
className: "visited-no-recolor"
|
|
45172
|
-
}, strings.videoTranscript));
|
|
45173
|
-
}
|
|
45174
|
-
case "error":
|
|
45175
|
-
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
|
|
45176
|
-
case "aborted":
|
|
45177
|
-
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
|
|
45178
|
-
default:
|
|
45179
|
-
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
|
|
45180
|
-
}
|
|
45181
|
-
};
|
|
45182
|
-
const styles$4 = aphrodite.StyleSheet.create({
|
|
45183
|
-
transcriptLink: {
|
|
45184
|
-
flexDirection: "row",
|
|
45185
|
-
width: "100%",
|
|
45186
|
-
justifyContent: "center"
|
|
45187
|
-
}
|
|
45188
|
-
});
|
|
45189
|
-
|
|
45190
|
-
/**
|
|
45191
|
-
* This is a video widget for embedding videos in articles.
|
|
45192
|
-
*/
|
|
45193
|
-
// Current default is 720p, based on the typical videos we upload currently
|
|
45194
|
-
const DEFAULT_WIDTH = 1280;
|
|
45195
|
-
const DEFAULT_HEIGHT = 720;
|
|
45196
|
-
const KA_EMBED = "{host}/embed_video?slug={slug}" + "&internal_video_only=1";
|
|
45197
|
-
const IS_URL = /^https?:\/\//;
|
|
45198
|
-
const IS_KA_SITE = /(khanacademy\.org|localhost)/;
|
|
45199
|
-
const IS_VIMEO = /(vimeo\.com)/;
|
|
45200
|
-
|
|
45201
|
-
// exports has no 'transform'
|
|
45202
|
-
|
|
45203
|
-
/**
|
|
45204
|
-
* Video renderer.
|
|
45205
|
-
*/
|
|
45206
|
-
class Video extends React__namespace.Component {
|
|
45207
|
-
static contextType = PerseusI18nContext;
|
|
45208
|
-
/**
|
|
45209
|
-
* This is the widget's grading function.
|
|
45210
|
-
* Points for videos are tallied by the embedded video itself, in the case
|
|
45211
|
-
* of Khan Academy videos.
|
|
45212
|
-
*/
|
|
45213
|
-
static validate(userInput, rubric) {
|
|
45214
|
-
return {
|
|
45215
|
-
type: "points",
|
|
45216
|
-
earned: 0,
|
|
45217
|
-
total: 0,
|
|
45218
|
-
message: null
|
|
45219
|
-
};
|
|
45220
|
-
}
|
|
45221
|
-
getUserInput = () => {
|
|
45222
|
-
return null;
|
|
45223
|
-
};
|
|
45224
|
-
simpleValidate = rubric => {
|
|
45225
|
-
return Video.validate(null, rubric);
|
|
45226
|
-
};
|
|
45227
|
-
change = (() => {
|
|
45228
|
-
var _this = this;
|
|
45229
|
-
return function () {
|
|
45230
|
-
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
45231
|
-
args[_key] = arguments[_key];
|
|
45232
|
-
}
|
|
45233
|
-
// @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
|
|
45234
|
-
return change.apply(_this, args);
|
|
45235
|
-
};
|
|
45236
|
-
})();
|
|
45237
|
-
render() {
|
|
45238
|
-
const {
|
|
45239
|
-
InitialRequestUrl
|
|
45240
|
-
} = getDependencies();
|
|
45241
|
-
const location = this.props.location;
|
|
45242
|
-
if (!location) {
|
|
45243
|
-
return /*#__PURE__*/React__namespace.createElement("div", null);
|
|
45244
|
-
}
|
|
45245
|
-
let url;
|
|
45246
|
-
if (IS_URL.test(location)) {
|
|
45247
|
-
url = location;
|
|
45248
|
-
if (IS_VIMEO.test(url)) {
|
|
45249
|
-
// If this is a vimeo video, we need to add the query string
|
|
45250
|
-
// parameter "dnt" so that analytics/tracking cookies aren't set.
|
|
45251
|
-
// https://help.vimeo.com/hc/en-us/articles/12426260232977-Player-parameters-overview
|
|
45252
|
-
if (url.indexOf("?") === -1) {
|
|
45253
|
-
url += "?dnt=1";
|
|
45254
|
-
} else {
|
|
45255
|
-
url += "&dnt=1";
|
|
45256
|
-
}
|
|
45257
|
-
}
|
|
45258
|
-
} else {
|
|
45259
|
-
url = KA_EMBED.replace("{slug}", location);
|
|
45260
|
-
let embedHostname = "https://www.khanacademy.org";
|
|
45261
|
-
if (IS_KA_SITE.test(InitialRequestUrl.host)) {
|
|
45262
|
-
embedHostname = InitialRequestUrl.origin;
|
|
45263
|
-
}
|
|
45264
|
-
url = url.replace("{host}", embedHostname);
|
|
45265
|
-
}
|
|
45266
|
-
return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(FixedToResponsive, {
|
|
45267
|
-
width: DEFAULT_WIDTH,
|
|
45268
|
-
height: DEFAULT_HEIGHT
|
|
45269
|
-
// The key is here for the benefit of the editor, to ensure that
|
|
45270
|
-
// any changes cause a re-rendering of the frame.
|
|
45271
|
-
,
|
|
45272
|
-
key: location + this.props.alignment
|
|
45273
|
-
}, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
|
|
45274
|
-
style: a11y.srOnly
|
|
45275
|
-
}, this.context.strings.videoWrapper), /*#__PURE__*/React__namespace.createElement("iframe", {
|
|
45276
|
-
// TODO(joshuan): Consider not using iframes when we're
|
|
45277
|
-
// loading this from webapp. This iframe is problematic
|
|
45278
|
-
// for screenreaders.
|
|
45279
|
-
className: "perseus-video-widget",
|
|
45280
|
-
sandbox: "allow-same-origin allow-scripts",
|
|
45281
|
-
width: DEFAULT_WIDTH,
|
|
45282
|
-
height: DEFAULT_HEIGHT,
|
|
45283
|
-
src: url,
|
|
45284
|
-
allowFullScreen: true
|
|
45285
|
-
})), /*#__PURE__*/React__namespace.createElement(VideoTranscriptLink, {
|
|
45286
|
-
location: location
|
|
45287
|
-
}));
|
|
45288
|
-
}
|
|
45289
|
-
}
|
|
45290
|
-
var Video$1 = {
|
|
45291
|
-
name: "video",
|
|
45292
|
-
displayName: "Video",
|
|
45293
|
-
defaultAlignment: "block",
|
|
45294
|
-
supportedAlignments: ["block", "float-left", "float-right", "full-width"],
|
|
45295
|
-
widget: Video
|
|
45296
|
-
};
|
|
45297
|
-
|
|
45298
|
-
/* eslint-disable import/no-named-as-default */
|
|
45299
|
-
var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, PythonProgram$1, Sorter$1, Table$1, Unit, Video$1, DeprecatedStandin$1];
|
|
45300
|
-
|
|
45301
|
-
/**
|
|
45302
|
-
* This should be called by all clients, specifying whether extra widgets are
|
|
45303
|
-
* needed via `loadExtraWidgets`. It is idempotent, so it's not a problem to
|
|
45304
|
-
* call it multiple times.
|
|
45305
|
-
*
|
|
45306
|
-
* skipMathJax:
|
|
45307
|
-
* If false/undefined, MathJax will be configured, and the
|
|
45308
|
-
* promise will wait for MathJax to load (if it hasn't already).
|
|
45309
|
-
*/
|
|
45310
|
-
const init = function (options) {
|
|
45311
|
-
registerWidgets(basicWidgets);
|
|
45312
|
-
registerWidgets(extraWidgets);
|
|
45313
|
-
replaceDeprecatedWidgets();
|
|
45314
|
-
|
|
45315
|
-
// Pass skipMathJax: true if MathJax is already loaded and configured.
|
|
45316
|
-
const skipMathJax = options.skipMathJax;
|
|
45317
|
-
if (skipMathJax) {
|
|
45318
|
-
// @ts-expect-error - TS2322 - Type 'Promise<void>' is not assignable to type 'Promise<undefined>'.
|
|
45319
|
-
return Promise.resolve();
|
|
45320
|
-
}
|
|
45321
|
-
return new Promise((resolve, reject) => {
|
|
45322
|
-
MathJax.Hub.Config({
|
|
45323
|
-
messageStyle: "none",
|
|
45324
|
-
skipStartupTypeset: "none",
|
|
45325
|
-
"HTML-CSS": {
|
|
45326
|
-
availableFonts: ["TeX"],
|
|
45327
|
-
imageFont: null,
|
|
45328
|
-
scale: 100,
|
|
45329
|
-
showMathMenu: false
|
|
45330
|
-
}
|
|
45331
|
-
});
|
|
45332
|
-
MathJax.Hub.Configured();
|
|
45333
|
-
MathJax.Hub.Queue(resolve);
|
|
45334
|
-
});
|
|
45335
|
-
};
|
|
45336
|
-
|
|
45337
|
-
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45338
|
-
const libName = "@khanacademy/perseus";
|
|
45339
|
-
const libVersion = "26.1.0";
|
|
45340
|
-
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45341
|
-
|
|
45342
|
-
const apiVersion = {
|
|
45343
|
-
major: 12,
|
|
45344
|
-
minor: 0
|
|
45345
|
-
};
|
|
45346
|
-
const itemDataVersion = {
|
|
45347
|
-
major: 0,
|
|
45348
|
-
minor: 1
|
|
45349
|
-
};
|
|
45350
|
-
|
|
45351
|
-
var allWidgets = [...basicWidgets, ...extraWidgets];
|
|
45352
|
-
|
|
45353
|
-
registerWidgets(allWidgets);
|
|
45354
|
-
const ItemVersion = getVersionVector();
|
|
45355
|
-
ItemVersion["::renderer::"] = itemDataVersion;
|
|
45356
|
-
|
|
45357
|
-
class ArticleRenderer extends React__namespace.Component {
|
|
45358
|
-
static contextType = PerseusI18nContext;
|
|
45359
|
-
_currentFocus;
|
|
45360
|
-
sectionRenderers = [];
|
|
45361
|
-
static defaultProps = {
|
|
45362
|
-
apiOptions: ApiOptions.defaults,
|
|
45363
|
-
useNewStyles: false,
|
|
45364
|
-
linterContext: PerseusLinter__namespace.linterContextDefault
|
|
45365
|
-
};
|
|
45366
|
-
constructor(props) {
|
|
45367
|
-
super(props);
|
|
45368
|
-
}
|
|
45369
|
-
componentDidMount() {
|
|
45370
|
-
this._currentFocus = null;
|
|
45371
|
-
}
|
|
45372
|
-
shouldComponentUpdate(nextProps) {
|
|
45373
|
-
return nextProps !== this.props;
|
|
45374
|
-
}
|
|
45375
|
-
_handleFocusChange = (newFocusPath, oldFocusPath) => {
|
|
45376
|
-
// TODO(charlie): DRY this up--some of this logic is repeated in
|
|
45377
|
-
// ItemRenderer.
|
|
45378
|
-
if (newFocusPath) {
|
|
45379
|
-
this._setCurrentFocus(newFocusPath);
|
|
45380
|
-
} else {
|
|
45381
|
-
this._onRendererBlur(oldFocusPath);
|
|
45382
|
-
}
|
|
45383
|
-
};
|
|
45384
|
-
_setCurrentFocus = newFocusPath => {
|
|
45385
|
-
const {
|
|
45386
|
-
keypadElement,
|
|
45387
|
-
apiOptions
|
|
45388
|
-
} = this.props;
|
|
45389
|
-
const {
|
|
45390
|
-
isMobile
|
|
45391
|
-
} = apiOptions;
|
|
45392
|
-
const prevFocusPath = this._currentFocus;
|
|
45393
|
-
this._currentFocus = newFocusPath;
|
|
45394
|
-
|
|
45395
|
-
// Use the section prefix to extract the relevant Renderer's input
|
|
45396
|
-
// paths, so as to check whether the focused path represents an
|
|
45397
|
-
// input.
|
|
45398
|
-
let didFocusInput = false;
|
|
45399
|
-
let focusedInput;
|
|
45400
|
-
if (this._currentFocus) {
|
|
45401
|
-
const [sectionIndex, ...focusPath] = this._currentFocus;
|
|
45402
|
-
const inputPaths = this.sectionRenderers[sectionIndex].getInputPaths();
|
|
45403
|
-
didFocusInput = inputPaths.some(inputPath => {
|
|
45404
|
-
return Util.inputPathsEqual(inputPath, focusPath);
|
|
45405
|
-
});
|
|
45406
|
-
focusedInput = this.sectionRenderers[sectionIndex].getDOMNodeForPath(focusPath);
|
|
45407
|
-
}
|
|
45408
|
-
const {
|
|
45409
|
-
onFocusChange
|
|
45410
|
-
} = this.props.apiOptions;
|
|
45411
|
-
if (onFocusChange) {
|
|
45412
|
-
// Wait for the keypad to mount before getting the height
|
|
45413
|
-
setTimeout(() => {
|
|
45414
|
-
const keypadDomNode = keypadElement?.getDOMNode();
|
|
45415
|
-
const keypadHeight = keypadDomNode && didFocusInput ? keypadDomNode.getBoundingClientRect().height : 0;
|
|
45416
|
-
onFocusChange(this._currentFocus, prevFocusPath, keypadHeight, didFocusInput ? focusedInput : null);
|
|
45417
|
-
}, 0);
|
|
45418
|
-
}
|
|
45419
|
-
if (keypadElement && isMobile) {
|
|
45420
|
-
if (didFocusInput) {
|
|
45421
|
-
keypadElement.activate();
|
|
45422
|
-
} else {
|
|
45423
|
-
keypadElement.dismiss();
|
|
45424
|
-
}
|
|
45425
|
-
}
|
|
45426
|
-
};
|
|
45427
|
-
_onRendererBlur = blurPath => {
|
|
45428
|
-
const blurringFocusPath = this._currentFocus;
|
|
45429
|
-
|
|
45430
|
-
// Failsafe: abort if ID is different, because focus probably happened
|
|
45431
|
-
// before blur.
|
|
45432
|
-
if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
|
|
45433
|
-
return;
|
|
45434
|
-
}
|
|
45435
|
-
|
|
45436
|
-
// Wait until after any new focus events fire this tick before declaring
|
|
45437
|
-
// that nothing is focused, since if there were a focus change across
|
|
45438
|
-
// sections, we could receive the blur before the focus.
|
|
45439
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
45440
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
45441
|
-
setTimeout(() => {
|
|
45442
|
-
if (Util.inputPathsEqual(this._currentFocus, blurringFocusPath)) {
|
|
45443
|
-
this._setCurrentFocus(null);
|
|
45444
|
-
}
|
|
45445
|
-
});
|
|
45446
|
-
};
|
|
45447
|
-
blur = () => {
|
|
45448
|
-
if (this._currentFocus) {
|
|
45449
|
-
const [sectionIndex, ...inputPath] = this._currentFocus;
|
|
45450
|
-
this.sectionRenderers[sectionIndex].blurPath(inputPath);
|
|
45451
|
-
}
|
|
45452
|
-
};
|
|
45453
|
-
_sections = () => {
|
|
45454
|
-
const sections = Array.isArray(this.props.json) ? this.props.json : [this.props.json];
|
|
45455
|
-
|
|
45456
|
-
// In JIPT context we split sections to paragraphs in order match the
|
|
45457
|
-
// translatable strings found on Crowdin when rendering articles in
|
|
45458
|
-
// the WYSIWYG mode for translation. This is needed for the jipt.js
|
|
45459
|
-
// integration in order to attribute the rendered strings on Crowdin.
|
|
45460
|
-
if (getDependencies().JIPT.useJIPT) {
|
|
45461
|
-
const paragraphs = [];
|
|
45462
|
-
for (const section of sections) {
|
|
45463
|
-
JiptParagraphs.parseToArray(section.content).forEach(paragraph => {
|
|
45464
|
-
paragraphs.push({
|
|
45465
|
-
...section,
|
|
45466
|
-
content: paragraph
|
|
45467
|
-
});
|
|
45468
|
-
});
|
|
45469
|
-
}
|
|
45470
|
-
return paragraphs;
|
|
45471
|
-
}
|
|
45472
|
-
return sections;
|
|
45473
|
-
};
|
|
45474
|
-
render() {
|
|
45475
|
-
const apiOptions = {
|
|
45476
|
-
...ApiOptions.defaults,
|
|
45477
|
-
...this.props.apiOptions,
|
|
45478
|
-
isArticle: true
|
|
45479
|
-
};
|
|
45480
|
-
const classes = classNames__default["default"]({
|
|
45481
|
-
"framework-perseus": true,
|
|
45482
|
-
"perseus-article": true,
|
|
45483
|
-
// NOTE(charlie): For exercises, this is applied outside of Perseus
|
|
45484
|
-
// (in webapp).
|
|
45485
|
-
[ClassNames.MOBILE]: apiOptions.isMobile
|
|
45486
|
-
});
|
|
45487
|
-
|
|
45488
|
-
// TODO(alex): Add mobile api functions and pass them down here
|
|
45489
|
-
// We're using the index as the key here because we don't have a unique
|
|
45490
|
-
// identifier for each section. This should be fine as we never remove
|
|
45491
|
-
// or reorder sections.
|
|
45492
|
-
const sections = this._sections().map((section, sectionIndex) => {
|
|
45493
|
-
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
45494
|
-
key: sectionIndex,
|
|
45495
|
-
className: "clearfix"
|
|
45496
|
-
}, /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, section, {
|
|
45497
|
-
ref: elem => {
|
|
45498
|
-
if (elem) {
|
|
45499
|
-
this.sectionRenderers[sectionIndex] = elem;
|
|
45500
|
-
}
|
|
45501
|
-
},
|
|
45502
|
-
key: sectionIndex,
|
|
45503
|
-
key_: sectionIndex,
|
|
45504
|
-
keypadElement: this.props.keypadElement,
|
|
45505
|
-
apiOptions: {
|
|
45506
|
-
...apiOptions,
|
|
45507
|
-
onFocusChange: (newFocusPath, oldFocusPath) => {
|
|
45508
|
-
// Prefix the paths with the relevant section index,
|
|
45509
|
-
// so as to allow us to distinguish between
|
|
45510
|
-
// equivalently-named inputs across Renderers.
|
|
45511
|
-
this._handleFocusChange(newFocusPath && [sectionIndex].concat(newFocusPath), oldFocusPath && [sectionIndex].concat(oldFocusPath));
|
|
45512
|
-
}
|
|
45513
|
-
},
|
|
45514
|
-
linterContext: PerseusLinter__namespace.pushContextStack(this.props.linterContext, "article"),
|
|
45515
|
-
legacyPerseusLint: this.props.legacyPerseusLint,
|
|
45516
|
-
strings: this.context.strings
|
|
45517
|
-
})));
|
|
45518
|
-
});
|
|
45519
|
-
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
45520
|
-
className: classes
|
|
45521
|
-
}, /*#__PURE__*/React__namespace.createElement(DependenciesContext.Provider, {
|
|
45522
|
-
value: this.props.dependencies
|
|
45523
|
-
}, sections));
|
|
44607
|
+
return sections;
|
|
44608
|
+
};
|
|
44609
|
+
render() {
|
|
44610
|
+
const apiOptions = {
|
|
44611
|
+
...ApiOptions.defaults,
|
|
44612
|
+
...this.props.apiOptions,
|
|
44613
|
+
isArticle: true
|
|
44614
|
+
};
|
|
44615
|
+
const classes = classNames__default["default"]({
|
|
44616
|
+
"framework-perseus": true,
|
|
44617
|
+
"perseus-article": true,
|
|
44618
|
+
// NOTE(charlie): For exercises, this is applied outside of Perseus
|
|
44619
|
+
// (in webapp).
|
|
44620
|
+
[ClassNames.MOBILE]: apiOptions.isMobile
|
|
44621
|
+
});
|
|
44622
|
+
|
|
44623
|
+
// TODO(alex): Add mobile api functions and pass them down here
|
|
44624
|
+
// We're using the index as the key here because we don't have a unique
|
|
44625
|
+
// identifier for each section. This should be fine as we never remove
|
|
44626
|
+
// or reorder sections.
|
|
44627
|
+
const sections = this._sections().map((section, sectionIndex) => {
|
|
44628
|
+
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
44629
|
+
key: sectionIndex,
|
|
44630
|
+
className: "clearfix"
|
|
44631
|
+
}, /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, section, {
|
|
44632
|
+
ref: elem => {
|
|
44633
|
+
if (elem) {
|
|
44634
|
+
this.sectionRenderers[sectionIndex] = elem;
|
|
44635
|
+
}
|
|
44636
|
+
},
|
|
44637
|
+
key: sectionIndex,
|
|
44638
|
+
key_: sectionIndex,
|
|
44639
|
+
keypadElement: this.props.keypadElement,
|
|
44640
|
+
apiOptions: {
|
|
44641
|
+
...apiOptions,
|
|
44642
|
+
onFocusChange: (newFocusPath, oldFocusPath) => {
|
|
44643
|
+
// Prefix the paths with the relevant section index,
|
|
44644
|
+
// so as to allow us to distinguish between
|
|
44645
|
+
// equivalently-named inputs across Renderers.
|
|
44646
|
+
this._handleFocusChange(newFocusPath && [sectionIndex].concat(newFocusPath), oldFocusPath && [sectionIndex].concat(oldFocusPath));
|
|
44647
|
+
}
|
|
44648
|
+
},
|
|
44649
|
+
linterContext: PerseusLinter__namespace.pushContextStack(this.props.linterContext, "article"),
|
|
44650
|
+
legacyPerseusLint: this.props.legacyPerseusLint,
|
|
44651
|
+
strings: this.context.strings
|
|
44652
|
+
})));
|
|
44653
|
+
});
|
|
44654
|
+
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
44655
|
+
className: classes
|
|
44656
|
+
}, /*#__PURE__*/React__namespace.createElement(DependenciesContext.Provider, {
|
|
44657
|
+
value: this.props.dependencies
|
|
44658
|
+
}, sections));
|
|
45524
44659
|
}
|
|
45525
44660
|
}
|
|
45526
44661
|
|
|
@@ -46869,31 +46004,248 @@ function inferItemTreeShape(node) {
|
|
|
46869
46004
|
if (typeof node === "object" && node.__type === "hint") {
|
|
46870
46005
|
return shapes.hint;
|
|
46871
46006
|
}
|
|
46872
|
-
if (typeof node === "object") {
|
|
46873
|
-
const valueShapes = {};
|
|
46874
|
-
Object.keys(node).forEach(key => {
|
|
46875
|
-
valueShapes[key] = inferItemTreeShape(node[key]);
|
|
46876
|
-
});
|
|
46877
|
-
return shapes.shape(valueShapes);
|
|
46007
|
+
if (typeof node === "object") {
|
|
46008
|
+
const valueShapes = {};
|
|
46009
|
+
Object.keys(node).forEach(key => {
|
|
46010
|
+
valueShapes[key] = inferItemTreeShape(node[key]);
|
|
46011
|
+
});
|
|
46012
|
+
return shapes.shape(valueShapes);
|
|
46013
|
+
}
|
|
46014
|
+
throw new perseusCore.PerseusError(`unexpected multi-item node ${JSON.stringify(node)}`, perseusCore.Errors.InvalidInput);
|
|
46015
|
+
}
|
|
46016
|
+
|
|
46017
|
+
/**
|
|
46018
|
+
* Convert the given Item to an ItemTree, by unwrapping the `_multi` key.
|
|
46019
|
+
*/
|
|
46020
|
+
function itemToTree(item) {
|
|
46021
|
+
return item._multi;
|
|
46022
|
+
}
|
|
46023
|
+
|
|
46024
|
+
/**
|
|
46025
|
+
* Convert the given ItemTree to an Item, by wrapping it in the `_multi` key.
|
|
46026
|
+
*/
|
|
46027
|
+
function treeToItem(node) {
|
|
46028
|
+
return {
|
|
46029
|
+
_multi: node
|
|
46030
|
+
};
|
|
46031
|
+
}
|
|
46032
|
+
|
|
46033
|
+
var isObject = function (obj) {
|
|
46034
|
+
return obj === Object(obj);
|
|
46035
|
+
};
|
|
46036
|
+
var merge = function () {
|
|
46037
|
+
var obj = {};
|
|
46038
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
46039
|
+
var source = arguments[i];
|
|
46040
|
+
if (source) {
|
|
46041
|
+
for (var prop in source) {
|
|
46042
|
+
obj[prop] = source[prop];
|
|
46043
|
+
}
|
|
46044
|
+
}
|
|
46045
|
+
}
|
|
46046
|
+
return obj;
|
|
46047
|
+
};
|
|
46048
|
+
var clone = function (obj) {
|
|
46049
|
+
if (!isObject(obj)) {
|
|
46050
|
+
return obj;
|
|
46051
|
+
}
|
|
46052
|
+
return Array.isArray(obj) ? obj.slice() : merge(obj);
|
|
46053
|
+
};
|
|
46054
|
+
|
|
46055
|
+
/* TODO batch *all* mutations
|
|
46056
|
+
* idea: freeze / thaw implementations for all types
|
|
46057
|
+
* lens constructor thaws, freeze delegates to type's freeze
|
|
46058
|
+
*/
|
|
46059
|
+
|
|
46060
|
+
// equivalents, without requiring it
|
|
46061
|
+
// find the implementation to use for a given object
|
|
46062
|
+
var dispatch = function (x) {
|
|
46063
|
+
if (Array.isArray(x)) {
|
|
46064
|
+
return arr;
|
|
46065
|
+
} else if (isObject(x)) {
|
|
46066
|
+
return obj;
|
|
46067
|
+
} else if (typeof x === "string") {
|
|
46068
|
+
return str;
|
|
46069
|
+
}
|
|
46070
|
+
};
|
|
46071
|
+
|
|
46072
|
+
// This is underscore with a different name
|
|
46073
|
+
var lens = function (obj) {
|
|
46074
|
+
if (obj instanceof lens) {
|
|
46075
|
+
return obj;
|
|
46076
|
+
}
|
|
46077
|
+
if (!(this instanceof lens)) {
|
|
46078
|
+
return new lens(obj);
|
|
46079
|
+
}
|
|
46080
|
+
var ops = dispatch(obj);
|
|
46081
|
+
this._wrapped = ops.thaw ? ops.thaw(obj) : obj;
|
|
46082
|
+
};
|
|
46083
|
+
lens.prototype.freeze = function () {
|
|
46084
|
+
var obj = this._wrapped;
|
|
46085
|
+
var ops = dispatch(obj);
|
|
46086
|
+
return ops.freeze ? ops.freeze(obj) : obj;
|
|
46087
|
+
};
|
|
46088
|
+
lens.prototype.zoom = function (lensArr) {
|
|
46089
|
+
if (this._zoomStack === undefined) {
|
|
46090
|
+
this._zoomStack = [];
|
|
46091
|
+
}
|
|
46092
|
+
this._zoomStack.push({
|
|
46093
|
+
zoom: lensArr,
|
|
46094
|
+
wrapped: this._wrapped
|
|
46095
|
+
});
|
|
46096
|
+
this._wrapped = lens(this._wrapped).get(lensArr);
|
|
46097
|
+
return this;
|
|
46098
|
+
};
|
|
46099
|
+
lens.prototype.deZoom = function () {
|
|
46100
|
+
var frame = this._zoomStack.pop();
|
|
46101
|
+
this._wrapped = lens(frame.wrapped).set(frame.zoom, this._wrapped).freeze();
|
|
46102
|
+
return this;
|
|
46103
|
+
};
|
|
46104
|
+
lens.prototype.get = function (lensArr) {
|
|
46105
|
+
var obj = this._wrapped;
|
|
46106
|
+
for (var i = 0; i < lensArr.length; i++) {
|
|
46107
|
+
obj = dispatch(obj).get(obj, lensArr[i]);
|
|
46108
|
+
}
|
|
46109
|
+
return obj;
|
|
46110
|
+
};
|
|
46111
|
+
lens.prototype.mod = function (lensArr, f) {
|
|
46112
|
+
var obj = this._wrapped;
|
|
46113
|
+
var newObj = clone(obj);
|
|
46114
|
+
var ops = dispatch(obj);
|
|
46115
|
+
if (lensArr.length === 0) {
|
|
46116
|
+
this._wrapped = f(this._wrapped);
|
|
46117
|
+
} else if (lensArr.length === 1) {
|
|
46118
|
+
this._wrapped = ops.mod(newObj, lensArr[0], f);
|
|
46119
|
+
} else {
|
|
46120
|
+
var monocle = lensArr[0];
|
|
46121
|
+
var shortLens = lensArr.slice(1);
|
|
46122
|
+
|
|
46123
|
+
// newObj = ops.mod(obj[monocle], shortLens, f);
|
|
46124
|
+
|
|
46125
|
+
newObj[monocle] = lens(obj[monocle]).mod(shortLens, f).freeze();
|
|
46126
|
+
this._wrapped = newObj;
|
|
46127
|
+
}
|
|
46128
|
+
return this;
|
|
46129
|
+
};
|
|
46130
|
+
|
|
46131
|
+
// TODO - move to individual files
|
|
46132
|
+
lens.prototype.merge = function (lensArr, props) {
|
|
46133
|
+
this._wrapped = lens(this._wrapped).mod(lensArr, function (oldProps) {
|
|
46134
|
+
return merge(oldProps, props);
|
|
46135
|
+
}).freeze();
|
|
46136
|
+
return this;
|
|
46137
|
+
};
|
|
46138
|
+
|
|
46139
|
+
// Lens must have length >= 1 or there would be nothing to return
|
|
46140
|
+
lens.prototype.del = function (lensArr) {
|
|
46141
|
+
var obj = this._wrapped;
|
|
46142
|
+
var ops = dispatch(obj);
|
|
46143
|
+
if (lensArr.length === 1) {
|
|
46144
|
+
this._wrapped = ops.del(obj, lensArr[0]);
|
|
46145
|
+
} else {
|
|
46146
|
+
var monocle = lensArr[0];
|
|
46147
|
+
var shortLens = lensArr.slice(1);
|
|
46148
|
+
var newObj = clone(obj);
|
|
46149
|
+
newObj[monocle] = lens(obj[monocle]).del(shortLens).freeze();
|
|
46150
|
+
this._wrapped = newObj;
|
|
46151
|
+
}
|
|
46152
|
+
return this;
|
|
46153
|
+
};
|
|
46154
|
+
lens.prototype.set = function (lensArr, set) {
|
|
46155
|
+
return this.mod(lensArr, function () {
|
|
46156
|
+
return set;
|
|
46157
|
+
});
|
|
46158
|
+
};
|
|
46159
|
+
|
|
46160
|
+
// ARRAY FUNCTIONS
|
|
46161
|
+
|
|
46162
|
+
const arr = {
|
|
46163
|
+
get: function (arr, monocle) {
|
|
46164
|
+
return arr[monocle];
|
|
46165
|
+
},
|
|
46166
|
+
set: function (arr, monocle, val) {
|
|
46167
|
+
var newArr = arr.splice();
|
|
46168
|
+
newArr[monocle] = val;
|
|
46169
|
+
return newArr;
|
|
46170
|
+
},
|
|
46171
|
+
mod: function (arr, monocle, f) {
|
|
46172
|
+
var newArr = arr.slice();
|
|
46173
|
+
newArr[monocle] = f(arr[monocle]);
|
|
46174
|
+
return newArr;
|
|
46175
|
+
},
|
|
46176
|
+
del: function (arr, monocle) {
|
|
46177
|
+
var newArr = arr.slice();
|
|
46178
|
+
newArr.splice(monocle, 1);
|
|
46179
|
+
return newArr;
|
|
46180
|
+
}
|
|
46181
|
+
};
|
|
46182
|
+
|
|
46183
|
+
/*
|
|
46184
|
+
// Lens must point to a member of an array. We'll insert into that array.
|
|
46185
|
+
lens.prototype.insertAt = function(lensArr, toInsert) {
|
|
46186
|
+
var obj = this._wrapped;
|
|
46187
|
+
|
|
46188
|
+
var arrLens = lensArr.slice(0, -1);
|
|
46189
|
+
var arr = lens(obj).get(arrLens).slice(); // slice to copy
|
|
46190
|
+
|
|
46191
|
+
var arrIdx = lensArr[lensArr.length-1];
|
|
46192
|
+
arr.splice(arrIdx, 0, toInsert);
|
|
46193
|
+
return lens(obj).set(arrLens, arr);
|
|
46194
|
+
};
|
|
46195
|
+
|
|
46196
|
+
lens.prototype.insertBefore = lens.prototype.insertAt;
|
|
46197
|
+
lens.prototype.insertAfter = function(lensArr, toInsert) {
|
|
46198
|
+
var newLens = lensArr.slice();
|
|
46199
|
+
newLens[newLens.length-1] += 1;
|
|
46200
|
+
return lens(this._wrapped).insertAt(newLens, toInsert);
|
|
46201
|
+
};
|
|
46202
|
+
*/
|
|
46203
|
+
|
|
46204
|
+
// OBJECT FUNCTIONS
|
|
46205
|
+
|
|
46206
|
+
const obj = {
|
|
46207
|
+
get: function (obj, monocle) {
|
|
46208
|
+
return obj[monocle];
|
|
46209
|
+
},
|
|
46210
|
+
set: function (obj, monocle, val) {
|
|
46211
|
+
var newObj = clone(obj);
|
|
46212
|
+
newObj[monocle] = val;
|
|
46213
|
+
return newObj;
|
|
46214
|
+
},
|
|
46215
|
+
mod: function (obj, monocle, f) {
|
|
46216
|
+
var newObj = clone(obj);
|
|
46217
|
+
newObj[monocle] = f(obj[monocle]);
|
|
46218
|
+
return newObj;
|
|
46219
|
+
},
|
|
46220
|
+
del: function (obj, monocle) {
|
|
46221
|
+
var newObj = clone(obj);
|
|
46222
|
+
delete newObj[monocle];
|
|
46223
|
+
return newObj;
|
|
46878
46224
|
}
|
|
46879
|
-
|
|
46880
|
-
}
|
|
46225
|
+
};
|
|
46881
46226
|
|
|
46882
|
-
|
|
46883
|
-
* Convert the given Item to an ItemTree, by unwrapping the `_multi` key.
|
|
46884
|
-
*/
|
|
46885
|
-
function itemToTree(item) {
|
|
46886
|
-
return item._multi;
|
|
46887
|
-
}
|
|
46227
|
+
// STRING FUNCTIONS
|
|
46888
46228
|
|
|
46889
|
-
|
|
46890
|
-
|
|
46891
|
-
|
|
46892
|
-
|
|
46893
|
-
|
|
46894
|
-
|
|
46895
|
-
|
|
46896
|
-
|
|
46229
|
+
const str = {
|
|
46230
|
+
get: function (arr, monocle) {
|
|
46231
|
+
return arr[monocle];
|
|
46232
|
+
},
|
|
46233
|
+
set: function (arr, monocle, val) {
|
|
46234
|
+
var newArr = arr.splice();
|
|
46235
|
+
newArr[monocle] = val;
|
|
46236
|
+
return newArr;
|
|
46237
|
+
},
|
|
46238
|
+
mod: function (arr, monocle, f) {
|
|
46239
|
+
var newArr = arr.splice();
|
|
46240
|
+
newArr[monocle] = f(arr[monocle]);
|
|
46241
|
+
return newArr;
|
|
46242
|
+
},
|
|
46243
|
+
del: function (arr, monocle) {
|
|
46244
|
+
var newArr = arr.slice();
|
|
46245
|
+
newArr.splice(monocle);
|
|
46246
|
+
return newArr;
|
|
46247
|
+
}
|
|
46248
|
+
};
|
|
46897
46249
|
|
|
46898
46250
|
// TODO(mdr)
|
|
46899
46251
|
// TODO(mdr)
|
|
@@ -47035,467 +46387,833 @@ class MultiRenderer extends React__namespace.Component {
|
|
|
47035
46387
|
}
|
|
47036
46388
|
|
|
47037
46389
|
/**
|
|
47038
|
-
* Construct a Renderer for the given HintNode, and keep track of the hint
|
|
47039
|
-
* itself for future use, too.
|
|
46390
|
+
* Construct a Renderer for the given HintNode, and keep track of the hint
|
|
46391
|
+
* itself for future use, too.
|
|
46392
|
+
*/
|
|
46393
|
+
_makeHintRendererData(hint) {
|
|
46394
|
+
// TODO(mdr): Once HintsRenderer supports inter-widget communication,
|
|
46395
|
+
// give it a ref. Until then, leave the ref null forever, to avoid
|
|
46396
|
+
// confusing the findWidgets functions.
|
|
46397
|
+
//
|
|
46398
|
+
// NOTE(davidflanagan): As a partial step toward inter-widget
|
|
46399
|
+
// communication we're going to pass a findExternalWidgets function
|
|
46400
|
+
// (using a dummy data object). This allows passage-ref widgets in
|
|
46401
|
+
// hints to use findWidget() to find the passage widgets they reference.
|
|
46402
|
+
// Note that this is one-way only, however. It does not allow
|
|
46403
|
+
// widgets in the question to find widgets in the hints, for example.
|
|
46404
|
+
const findExternalWidgets = criterion => this._findWidgets({}, criterion);
|
|
46405
|
+
return {
|
|
46406
|
+
hint,
|
|
46407
|
+
findExternalWidgets,
|
|
46408
|
+
// _annotateRendererArray() needs this
|
|
46409
|
+
ref: null,
|
|
46410
|
+
makeRenderer: () => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
|
|
46411
|
+
findExternalWidgets: findExternalWidgets,
|
|
46412
|
+
hints: [hint]
|
|
46413
|
+
}))
|
|
46414
|
+
};
|
|
46415
|
+
}
|
|
46416
|
+
|
|
46417
|
+
/**
|
|
46418
|
+
* Construct a tree of interconnected RendererDatas, corresponding to the
|
|
46419
|
+
* given item. Called in `_tryMakeRendererState`, in order to store this
|
|
46420
|
+
* tree in the component state.
|
|
46421
|
+
*/
|
|
46422
|
+
_makeRendererDataTree(item, shape) {
|
|
46423
|
+
const itemTree = itemToTree(item);
|
|
46424
|
+
return this.rendererDataTreeMapper.mapTree(itemTree, shape);
|
|
46425
|
+
}
|
|
46426
|
+
|
|
46427
|
+
/**
|
|
46428
|
+
* Return all widgets that meet the given criterion, from all Renderers
|
|
46429
|
+
* except the Renderer that triggered this call.
|
|
46430
|
+
*
|
|
46431
|
+
* This function is provided to each Renderer's `findExternalWidgets` prop,
|
|
46432
|
+
* which enables widgets in different Renderers to discover each other and
|
|
46433
|
+
* communicate.
|
|
46434
|
+
*/
|
|
46435
|
+
_findWidgets(callingData, filterCriterion) {
|
|
46436
|
+
const results = [];
|
|
46437
|
+
this._mapRenderers(data => {
|
|
46438
|
+
if (callingData !== data && data.ref) {
|
|
46439
|
+
results.push(...data.ref.findInternalWidgets(filterCriterion));
|
|
46440
|
+
}
|
|
46441
|
+
});
|
|
46442
|
+
return results;
|
|
46443
|
+
}
|
|
46444
|
+
|
|
46445
|
+
/**
|
|
46446
|
+
* Copy the renderer tree, apply the given transformation to the leaf nodes
|
|
46447
|
+
* and the optional given transformation to the array nodes, and return the
|
|
46448
|
+
* result.
|
|
46449
|
+
*
|
|
46450
|
+
* Used to provide structured data to the call site (the Renderer tree on
|
|
46451
|
+
* `render`, the Score tree on `getScores`, etc.), and to traverse the
|
|
46452
|
+
* renderer tree even when we disregard the output (like in
|
|
46453
|
+
* `_findWidgets`).
|
|
46454
|
+
*/
|
|
46455
|
+
_mapRenderers(
|
|
46456
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
46457
|
+
leafMapper
|
|
46458
|
+
// @ts-expect-error - TS2315 - Type 'Tree' is not generic.
|
|
46459
|
+
) {
|
|
46460
|
+
const {
|
|
46461
|
+
rendererDataTree
|
|
46462
|
+
} = this.state;
|
|
46463
|
+
if (!rendererDataTree) {
|
|
46464
|
+
return null;
|
|
46465
|
+
}
|
|
46466
|
+
const mapper = buildMapper().setContentMapper(leafMapper).setHintMapper(leafMapper);
|
|
46467
|
+
return mapper.mapTree(rendererDataTree, this.props.shape);
|
|
46468
|
+
}
|
|
46469
|
+
_scoreFromRef(ref) {
|
|
46470
|
+
if (!ref) {
|
|
46471
|
+
return null;
|
|
46472
|
+
}
|
|
46473
|
+
const [guess, score] = ref.guessAndScore();
|
|
46474
|
+
let state;
|
|
46475
|
+
if (ref.getSerializedState) {
|
|
46476
|
+
state = ref.getSerializedState();
|
|
46477
|
+
}
|
|
46478
|
+
return Util.keScoreFromPerseusScore(score, guess, state);
|
|
46479
|
+
}
|
|
46480
|
+
|
|
46481
|
+
/**
|
|
46482
|
+
* Return a tree in the shape of the multi-item, with scores at each of
|
|
46483
|
+
* the content nodes and `null` at the other leaf nodes.
|
|
46484
|
+
*/
|
|
46485
|
+
getScores() {
|
|
46486
|
+
return this._mapRenderers(data => this._scoreFromRef(data.ref));
|
|
46487
|
+
}
|
|
46488
|
+
|
|
46489
|
+
/**
|
|
46490
|
+
* Return a single composite score for all rendered content nodes.
|
|
46491
|
+
* The `guess` is a tree in the shape of the multi-item, with an individual
|
|
46492
|
+
* guess at each content node and `null` at the other leaf nodes.
|
|
46493
|
+
*/
|
|
46494
|
+
score() {
|
|
46495
|
+
const scores = [];
|
|
46496
|
+
const state = [];
|
|
46497
|
+
const guess = this._mapRenderers(data => {
|
|
46498
|
+
if (!data.ref) {
|
|
46499
|
+
return null;
|
|
46500
|
+
}
|
|
46501
|
+
if (data.ref.getSerializedState) {
|
|
46502
|
+
state.push(data.ref.getSerializedState());
|
|
46503
|
+
}
|
|
46504
|
+
scores.push(data.ref.score());
|
|
46505
|
+
return data.ref?.getUserInput();
|
|
46506
|
+
});
|
|
46507
|
+
const combinedScore = scores.reduce(Util.combineScores);
|
|
46508
|
+
return Util.keScoreFromPerseusScore(combinedScore, guess, state);
|
|
46509
|
+
}
|
|
46510
|
+
|
|
46511
|
+
/**
|
|
46512
|
+
* Return a tree in the shape of the multi-item, with serialized state at
|
|
46513
|
+
* each of the content nodes and `null` at the other leaf nodes.
|
|
46514
|
+
*
|
|
46515
|
+
* If the lastSerializedState argument is supplied, this function will fill
|
|
46516
|
+
* in the state of not-currently-rendered content and hint nodes with the
|
|
46517
|
+
* values from the previous serialized state. If no lastSerializedState is
|
|
46518
|
+
* supplied, `null` will be returned for not-currently-rendered content and
|
|
46519
|
+
* hint nodes.
|
|
46520
|
+
*/
|
|
46521
|
+
_getSerializedState(lastSerializedState) {
|
|
46522
|
+
return this._mapRenderers((data, _, path) => {
|
|
46523
|
+
if (data.ref) {
|
|
46524
|
+
return data.ref.getSerializedState();
|
|
46525
|
+
}
|
|
46526
|
+
if (lastSerializedState) {
|
|
46527
|
+
return lens(lastSerializedState).get(path);
|
|
46528
|
+
}
|
|
46529
|
+
return null;
|
|
46530
|
+
});
|
|
46531
|
+
}
|
|
46532
|
+
|
|
46533
|
+
/**
|
|
46534
|
+
* Given a tree in the shape of the multi-item, with serialized state at
|
|
46535
|
+
* each of the content nodes, restore each state to the corresponding
|
|
46536
|
+
* renderer if currently mounted.
|
|
46537
|
+
*/
|
|
46538
|
+
restoreSerializedState(serializedState, callback) {
|
|
46539
|
+
// We want to call our async callback only once all of the childrens'
|
|
46540
|
+
// callbacks have run. We add one to this counter before we call out to
|
|
46541
|
+
// each renderer and decrement it when it runs our callback.
|
|
46542
|
+
let numCallbacks = 0;
|
|
46543
|
+
const countCallback = () => {
|
|
46544
|
+
numCallbacks--;
|
|
46545
|
+
if (callback && numCallbacks === 0) {
|
|
46546
|
+
callback();
|
|
46547
|
+
}
|
|
46548
|
+
};
|
|
46549
|
+
this._mapRenderers((data, _, path) => {
|
|
46550
|
+
if (!data.ref) {
|
|
46551
|
+
return;
|
|
46552
|
+
}
|
|
46553
|
+
const state = lens(serializedState).get(path);
|
|
46554
|
+
if (!state) {
|
|
46555
|
+
return;
|
|
46556
|
+
}
|
|
46557
|
+
numCallbacks++;
|
|
46558
|
+
data.ref?.restoreSerializedState(state, countCallback);
|
|
46559
|
+
});
|
|
46560
|
+
}
|
|
46561
|
+
|
|
46562
|
+
/**
|
|
46563
|
+
* Given an array of renderers, if it happens to be an array of *hint*
|
|
46564
|
+
* renderers, then attach a `firstN` method to the array, which allows the
|
|
46565
|
+
* layout to render the hints together in one HintsRenderer.
|
|
47040
46566
|
*/
|
|
47041
|
-
|
|
47042
|
-
|
|
47043
|
-
|
|
47044
|
-
|
|
47045
|
-
|
|
47046
|
-
|
|
47047
|
-
|
|
47048
|
-
|
|
47049
|
-
|
|
47050
|
-
|
|
47051
|
-
|
|
47052
|
-
|
|
47053
|
-
return
|
|
47054
|
-
hint,
|
|
47055
|
-
findExternalWidgets,
|
|
47056
|
-
// _annotateRendererArray() needs this
|
|
47057
|
-
ref: null,
|
|
47058
|
-
makeRenderer: () => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
|
|
47059
|
-
findExternalWidgets: findExternalWidgets,
|
|
47060
|
-
hints: [hint]
|
|
47061
|
-
}))
|
|
47062
|
-
};
|
|
46567
|
+
_annotateRendererArray(renderers, rendererDatas, shape) {
|
|
46568
|
+
if (shape.elementShape.type === "hint") {
|
|
46569
|
+
// The shape says that these are HintRendererDatas, even though
|
|
46570
|
+
// it's not provable at compile time, so perform a cast.
|
|
46571
|
+
const hintRendererDatas = rendererDatas;
|
|
46572
|
+
renderers = [...renderers];
|
|
46573
|
+
renderers.firstN = n => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
|
|
46574
|
+
findExternalWidgets: hintRendererDatas[0] ? hintRendererDatas[0].findExternalWidgets : undefined,
|
|
46575
|
+
hints: hintRendererDatas.map(d => d.hint),
|
|
46576
|
+
hintsVisible: n
|
|
46577
|
+
}));
|
|
46578
|
+
}
|
|
46579
|
+
return renderers;
|
|
47063
46580
|
}
|
|
47064
46581
|
|
|
47065
46582
|
/**
|
|
47066
|
-
*
|
|
47067
|
-
*
|
|
47068
|
-
*
|
|
46583
|
+
* Return a tree in the shape of the multi-item, with a Renderer at each
|
|
46584
|
+
* content node and a HintRenderer at each hint node.
|
|
46585
|
+
*
|
|
46586
|
+
* This is generated by running each of the `makeRenderer` functions at the
|
|
46587
|
+
* leaf nodes.
|
|
47069
46588
|
*/
|
|
47070
|
-
|
|
47071
|
-
|
|
47072
|
-
|
|
46589
|
+
_getRenderers() {
|
|
46590
|
+
return this.getRenderersMapper.mapTree(this.state.rendererDataTree, this.props.shape);
|
|
46591
|
+
}
|
|
46592
|
+
render() {
|
|
46593
|
+
if (this.state.renderError) {
|
|
46594
|
+
return /*#__PURE__*/React__namespace.createElement("div", {
|
|
46595
|
+
className: aphrodite.css(styles.error)
|
|
46596
|
+
}, this.context.strings.errorRendering({
|
|
46597
|
+
error: String(this.state.renderError)
|
|
46598
|
+
}));
|
|
46599
|
+
}
|
|
46600
|
+
|
|
46601
|
+
// Pass the renderer tree to the `children` function, which will
|
|
46602
|
+
// determine the actual content of this component.
|
|
46603
|
+
return /*#__PURE__*/React__namespace.createElement(DependenciesContext.Provider, {
|
|
46604
|
+
value: this.props.dependencies
|
|
46605
|
+
}, this.props.children({
|
|
46606
|
+
renderers: this._getRenderers()
|
|
46607
|
+
}));
|
|
46608
|
+
}
|
|
46609
|
+
}
|
|
46610
|
+
const styles = aphrodite.StyleSheet.create({
|
|
46611
|
+
error: {
|
|
46612
|
+
color: "red"
|
|
46613
|
+
}
|
|
46614
|
+
});
|
|
46615
|
+
|
|
46616
|
+
/**
|
|
46617
|
+
* Utility functions to build React PropTypes for multi-items and shapes.
|
|
46618
|
+
*
|
|
46619
|
+
* If you're writing new components, though, consider using the Item and Shape
|
|
46620
|
+
* types instead.
|
|
46621
|
+
*/
|
|
46622
|
+
|
|
46623
|
+
/**
|
|
46624
|
+
* Return a PropType that accepts Items of the given shape, and rejects other
|
|
46625
|
+
* objects.
|
|
46626
|
+
*
|
|
46627
|
+
* Usage: `propTypes: {item: buildPropTypeForShape(myShape)}`
|
|
46628
|
+
*/
|
|
46629
|
+
function buildPropTypeForShape(shape) {
|
|
46630
|
+
return PropTypes__default["default"].oneOfType([PropTypes__default["default"].shape({
|
|
46631
|
+
_multi: buildTreePropTypeForShape(shape)
|
|
46632
|
+
}), PropTypes__default["default"].oneOf([null, undefined])]);
|
|
46633
|
+
}
|
|
46634
|
+
|
|
46635
|
+
/**
|
|
46636
|
+
* Return a PropType that accepts ItemTrees of the given shape, and rejects
|
|
46637
|
+
* other objects.
|
|
46638
|
+
*/
|
|
46639
|
+
function buildTreePropTypeForShape(shape) {
|
|
46640
|
+
if (shape.type === "content") {
|
|
46641
|
+
return PropTypes__default["default"].shape({
|
|
46642
|
+
// TODO(mdr): Remove #LegacyContentNode support.
|
|
46643
|
+
__type: PropTypes__default["default"].oneOf(["content", "item"]).isRequired,
|
|
46644
|
+
content: PropTypes__default["default"].string,
|
|
46645
|
+
images: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
|
|
46646
|
+
widgets: PropTypes__default["default"].objectOf(PropTypes__default["default"].any)
|
|
46647
|
+
});
|
|
46648
|
+
}
|
|
46649
|
+
if (shape.type === "hint") {
|
|
46650
|
+
return PropTypes__default["default"].shape({
|
|
46651
|
+
__type: PropTypes__default["default"].oneOf(["hint"]).isRequired,
|
|
46652
|
+
content: PropTypes__default["default"].string,
|
|
46653
|
+
images: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
|
|
46654
|
+
widgets: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
|
|
46655
|
+
replace: PropTypes__default["default"].bool
|
|
46656
|
+
});
|
|
46657
|
+
}
|
|
46658
|
+
if (shape.type === "tags") {
|
|
46659
|
+
return PropTypes__default["default"].arrayOf(PropTypes__default["default"].string.isRequired);
|
|
46660
|
+
}
|
|
46661
|
+
if (shape.type === "array") {
|
|
46662
|
+
const elementPropType = buildTreePropTypeForShape(shape.elementShape);
|
|
46663
|
+
return PropTypes__default["default"].arrayOf(elementPropType.isRequired);
|
|
46664
|
+
}
|
|
46665
|
+
if (shape.type === "object") {
|
|
46666
|
+
const valueShapes = shape.shape;
|
|
46667
|
+
const propTypeShape = {};
|
|
46668
|
+
Object.keys(valueShapes).forEach(key => {
|
|
46669
|
+
propTypeShape[key] = buildTreePropTypeForShape(valueShapes[key]).isRequired;
|
|
46670
|
+
});
|
|
46671
|
+
return PropTypes__default["default"].shape(propTypeShape);
|
|
46672
|
+
}
|
|
46673
|
+
throw new perseusCore.PerseusError(
|
|
46674
|
+
// @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
|
|
46675
|
+
`unexpected shape type ${shape.type}`, perseusCore.Errors.InvalidInput);
|
|
46676
|
+
}
|
|
46677
|
+
|
|
46678
|
+
/**
|
|
46679
|
+
* This library provides support for Perseus multi-items: structured Perseus
|
|
46680
|
+
* content that content creators can easily create, and that applications can
|
|
46681
|
+
* easily render into different parts of the layout.
|
|
46682
|
+
*
|
|
46683
|
+
* For more details about application and motivation, see:
|
|
46684
|
+
* https://sites.google.com/a/khanacademy.org/forge/for-developers/perseus-items-and-multi-items
|
|
46685
|
+
*
|
|
46686
|
+
* This file primarily exposes the `MultiRenderer` component, which performs
|
|
46687
|
+
* multi-rendering. To multi-render a question, pass in the content of the item
|
|
46688
|
+
* to the `MultiRenderer` component as a props. Then, pass in a function which
|
|
46689
|
+
* takes an object of renderers (in the same structure as the content), and
|
|
46690
|
+
* return a render tree. The `MultiRenderer` component will allow you to
|
|
46691
|
+
* combine scores, serialized state, etc. without having to manually call on
|
|
46692
|
+
* each of the functions. It also handles inter-widgets requests between the
|
|
46693
|
+
* different renderers.
|
|
46694
|
+
* For more details, see `multi-items/multi-renderer.jsx`.
|
|
46695
|
+
*
|
|
46696
|
+
* Example:
|
|
46697
|
+
*
|
|
46698
|
+
* item = {_multi: {
|
|
46699
|
+
* left: <content data>,
|
|
46700
|
+
* right: [<content data>, <content data>],
|
|
46701
|
+
* }}
|
|
46702
|
+
* shape = shapes.shape({
|
|
46703
|
+
* left: shapes.content,
|
|
46704
|
+
* right: shapes.arrayOf(shapes.content),
|
|
46705
|
+
* })
|
|
46706
|
+
*
|
|
46707
|
+
* <MultiRenderer item={item} shape={shape}>
|
|
46708
|
+
* {({renderers}) =>
|
|
46709
|
+
* <div>
|
|
46710
|
+
* <div id="left">{renderers.left}</div>
|
|
46711
|
+
* <ul id="right">
|
|
46712
|
+
* {renderers.right.map(r => <li>{r}</li>)}
|
|
46713
|
+
* </ul>
|
|
46714
|
+
* </div>
|
|
46715
|
+
* }
|
|
46716
|
+
* </MultiRenderer>
|
|
46717
|
+
*
|
|
46718
|
+
* This file also exposes `shapes`, which helps you construct a runtime type
|
|
46719
|
+
* declaration for your particular class of multi-item. This can then be used
|
|
46720
|
+
* to create a MultirendererEditor for your multi-item shape, and to validate
|
|
46721
|
+
* that a multi-item conforms to the shape via `buildPropTypeForShape`.
|
|
46722
|
+
* For more details, see `multi-items/shapes.js`.
|
|
46723
|
+
*
|
|
46724
|
+
* This file also exposes some utility functions for working with generic
|
|
46725
|
+
* multi-items, like `findContentNodesInItem`, `findHintNodesInItem`,
|
|
46726
|
+
* `inferItemShape`, and `buildEmptyItemForShape`.
|
|
46727
|
+
* For more details, see `multi-items/items.js`.
|
|
46728
|
+
*/
|
|
46729
|
+
var MultiItems = {
|
|
46730
|
+
// Tools for rendering your multi-items
|
|
46731
|
+
MultiRenderer,
|
|
46732
|
+
// Tools for declaring your multi-item shapes
|
|
46733
|
+
shapes,
|
|
46734
|
+
buildPropTypeForShape,
|
|
46735
|
+
// Tools for generically manipulating multi-items
|
|
46736
|
+
buildEmptyItemForShape,
|
|
46737
|
+
findContentNodesInItem: findContentNodesInItem$2,
|
|
46738
|
+
findHintNodesInItem,
|
|
46739
|
+
inferItemShape: inferItemShape$2
|
|
46740
|
+
};
|
|
46741
|
+
|
|
46742
|
+
/**
|
|
46743
|
+
* Calculates whether a perseus item is renderable by a specific
|
|
46744
|
+
* perseus-item-data version.
|
|
46745
|
+
*
|
|
46746
|
+
* This is done inside of the perseus repo so that it can traverse
|
|
46747
|
+
* widget-specific data that might need to do a sub-traversal.
|
|
46748
|
+
* This supports widgets that contain renderers, such as the
|
|
46749
|
+
* group or sequence widgets.
|
|
46750
|
+
*/
|
|
46751
|
+
const {
|
|
46752
|
+
findContentNodesInItem: findContentNodesInItem$1,
|
|
46753
|
+
inferItemShape: inferItemShape$1
|
|
46754
|
+
} = MultiItems;
|
|
46755
|
+
const isUpgradedWidgetInfoRenderableBy = function (widgetInfo, widgetRendererVersion) {
|
|
46756
|
+
if (widgetRendererVersion == null) {
|
|
46757
|
+
// If the widget does not exist in this version, this will
|
|
46758
|
+
// be null, and that version of perseus cannot render the
|
|
46759
|
+
// widget (it doesn't even know the widget exists!)
|
|
46760
|
+
return false;
|
|
46761
|
+
}
|
|
46762
|
+
const widgetVersion = widgetInfo.version || {
|
|
46763
|
+
major: 0,
|
|
46764
|
+
minor: 0
|
|
46765
|
+
};
|
|
46766
|
+
if (widgetRendererVersion.major > widgetVersion.major) {
|
|
46767
|
+
return true;
|
|
46768
|
+
}
|
|
46769
|
+
if (widgetRendererVersion.major < widgetVersion.major) {
|
|
46770
|
+
return false;
|
|
46771
|
+
}
|
|
46772
|
+
// If the major versions are the same, the minor version acts
|
|
46773
|
+
// like a tie-breaker.
|
|
46774
|
+
// For example, input-number 3.2 can render an input-number
|
|
46775
|
+
// 2.4, 3.0, or 3.2, but not an input number 3.3 or 4.0.
|
|
46776
|
+
return widgetRendererVersion.minor >= widgetVersion.minor;
|
|
46777
|
+
};
|
|
46778
|
+
const isRawWidgetInfoRenderableBy = function (widgetInfo, rendererContentVersion) {
|
|
46779
|
+
// Empty/non-existant widgets are always safe to render
|
|
46780
|
+
if (widgetInfo == null || widgetInfo.type == null) {
|
|
46781
|
+
return true;
|
|
47073
46782
|
}
|
|
47074
46783
|
|
|
47075
|
-
|
|
47076
|
-
|
|
47077
|
-
|
|
47078
|
-
|
|
47079
|
-
|
|
47080
|
-
|
|
47081
|
-
|
|
47082
|
-
|
|
47083
|
-
|
|
47084
|
-
|
|
47085
|
-
|
|
47086
|
-
|
|
47087
|
-
|
|
46784
|
+
// NOTE: This doesn't modify the widget info if the widget info
|
|
46785
|
+
// is at a later version than is supported.
|
|
46786
|
+
const upgradedWidgetInfo = upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
46787
|
+
return isUpgradedWidgetInfoRenderableBy(upgradedWidgetInfo, rendererContentVersion[upgradedWidgetInfo.type]);
|
|
46788
|
+
};
|
|
46789
|
+
const isRendererContentRenderableBy = function (rendererOptions, rendererContentVersion) {
|
|
46790
|
+
let isRenderable = true;
|
|
46791
|
+
traverse(rendererOptions, null, function (widgetInfo) {
|
|
46792
|
+
isRenderable = isRenderable && isRawWidgetInfoRenderableBy(widgetInfo, rendererContentVersion);
|
|
46793
|
+
});
|
|
46794
|
+
return isRenderable;
|
|
46795
|
+
};
|
|
46796
|
+
const isItemRenderableByVersion = function (itemData, rendererContentVersion) {
|
|
46797
|
+
if (itemData == null || rendererContentVersion == null) {
|
|
46798
|
+
throw new perseusCore.PerseusError("missing parameter to Perseus.isRenderable.item", perseusCore.Errors.InvalidInput);
|
|
46799
|
+
}
|
|
46800
|
+
if (itemData._multi) {
|
|
46801
|
+
const shape = inferItemShape$1(itemData);
|
|
46802
|
+
let isRenderable = true;
|
|
46803
|
+
findContentNodesInItem$1(itemData, shape, node => {
|
|
46804
|
+
const nodeIsRenderable = isRendererContentRenderableBy(node, rendererContentVersion);
|
|
46805
|
+
if (!nodeIsRenderable) {
|
|
46806
|
+
isRenderable = false;
|
|
47088
46807
|
}
|
|
47089
46808
|
});
|
|
47090
|
-
return
|
|
46809
|
+
return isRenderable;
|
|
47091
46810
|
}
|
|
46811
|
+
return isRendererContentRenderableBy(itemData.question, rendererContentVersion);
|
|
46812
|
+
};
|
|
47092
46813
|
|
|
47093
|
-
|
|
47094
|
-
|
|
47095
|
-
|
|
47096
|
-
|
|
47097
|
-
|
|
47098
|
-
|
|
47099
|
-
|
|
47100
|
-
|
|
47101
|
-
|
|
47102
|
-
|
|
47103
|
-
|
|
47104
|
-
|
|
47105
|
-
|
|
47106
|
-
|
|
47107
|
-
|
|
47108
|
-
const {
|
|
47109
|
-
rendererDataTree
|
|
47110
|
-
} = this.state;
|
|
47111
|
-
if (!rendererDataTree) {
|
|
47112
|
-
return null;
|
|
47113
|
-
}
|
|
47114
|
-
const mapper = buildMapper().setContentMapper(leafMapper).setHintMapper(leafMapper);
|
|
47115
|
-
return mapper.mapTree(rendererDataTree, this.props.shape);
|
|
47116
|
-
}
|
|
47117
|
-
_scoreFromRef(ref) {
|
|
47118
|
-
if (!ref) {
|
|
47119
|
-
return null;
|
|
47120
|
-
}
|
|
47121
|
-
const [guess, score] = ref.guessAndScore();
|
|
47122
|
-
let state;
|
|
47123
|
-
if (ref.getSerializedState) {
|
|
47124
|
-
state = ref.getSerializedState();
|
|
46814
|
+
/**
|
|
46815
|
+
* Identifies whether or not a given perseus item requires the use of a mouse
|
|
46816
|
+
* or screen, based on the widgets it contains.
|
|
46817
|
+
*/
|
|
46818
|
+
const {
|
|
46819
|
+
findContentNodesInItem,
|
|
46820
|
+
inferItemShape
|
|
46821
|
+
} = MultiItems;
|
|
46822
|
+
|
|
46823
|
+
// Iterate over a single Perseus renderer, mutating `widgets` by appending
|
|
46824
|
+
// violating widget types discovered in this item.
|
|
46825
|
+
function traverseRenderer(itemData, widgets) {
|
|
46826
|
+
traverse(itemData, null, function (info) {
|
|
46827
|
+
if (info.type && !isAccessible(info)) {
|
|
46828
|
+
widgets.push(info.type);
|
|
47125
46829
|
}
|
|
47126
|
-
|
|
47127
|
-
|
|
46830
|
+
});
|
|
46831
|
+
}
|
|
47128
46832
|
|
|
47129
|
-
|
|
47130
|
-
|
|
47131
|
-
|
|
47132
|
-
|
|
47133
|
-
|
|
47134
|
-
|
|
46833
|
+
// Returns a list of widgets that cause a given perseus item to require
|
|
46834
|
+
// the use of a screen or mouse.
|
|
46835
|
+
//
|
|
46836
|
+
// For now we'll just check the `accessible` field on each of the widgets
|
|
46837
|
+
// in the item data, but in the future we may specify accessibility on
|
|
46838
|
+
// each widget with higher granularity.
|
|
46839
|
+
function violatingWidgets(itemData) {
|
|
46840
|
+
// TODO(jordan): Hints as well
|
|
46841
|
+
const widgets = [];
|
|
46842
|
+
if (itemData._multi) {
|
|
46843
|
+
const shape = inferItemShape(itemData);
|
|
46844
|
+
findContentNodesInItem(itemData, shape, content => traverseRenderer(content, widgets));
|
|
46845
|
+
} else {
|
|
46846
|
+
traverseRenderer(itemData.question, widgets);
|
|
47135
46847
|
}
|
|
47136
46848
|
|
|
47137
|
-
|
|
47138
|
-
|
|
47139
|
-
|
|
47140
|
-
* guess at each content node and `null` at the other leaf nodes.
|
|
47141
|
-
*/
|
|
47142
|
-
score() {
|
|
47143
|
-
const scores = [];
|
|
47144
|
-
const state = [];
|
|
47145
|
-
const guess = this._mapRenderers(data => {
|
|
47146
|
-
if (!data.ref) {
|
|
47147
|
-
return null;
|
|
47148
|
-
}
|
|
47149
|
-
if (data.ref.getSerializedState) {
|
|
47150
|
-
state.push(data.ref.getSerializedState());
|
|
47151
|
-
}
|
|
47152
|
-
scores.push(data.ref.score());
|
|
47153
|
-
return data.ref?.getUserInput();
|
|
47154
|
-
});
|
|
47155
|
-
const combinedScore = scores.reduce(Util.combineScores);
|
|
47156
|
-
return Util.keScoreFromPerseusScore(combinedScore, guess, state);
|
|
47157
|
-
}
|
|
46849
|
+
// Uniquify the list of widgets (by type)
|
|
46850
|
+
return ___default["default"].uniq(widgets);
|
|
46851
|
+
}
|
|
47158
46852
|
|
|
47159
|
-
|
|
47160
|
-
* Return a tree in the shape of the multi-item, with serialized state at
|
|
47161
|
-
* each of the content nodes and `null` at the other leaf nodes.
|
|
47162
|
-
*
|
|
47163
|
-
* If the lastSerializedState argument is supplied, this function will fill
|
|
47164
|
-
* in the state of not-currently-rendered content and hint nodes with the
|
|
47165
|
-
* values from the previous serialized state. If no lastSerializedState is
|
|
47166
|
-
* supplied, `null` will be returned for not-currently-rendered content and
|
|
47167
|
-
* hint nodes.
|
|
47168
|
-
*/
|
|
47169
|
-
_getSerializedState(lastSerializedState) {
|
|
47170
|
-
return this._mapRenderers((data, _, path) => {
|
|
47171
|
-
if (data.ref) {
|
|
47172
|
-
return data.ref.getSerializedState();
|
|
47173
|
-
}
|
|
47174
|
-
if (lastSerializedState) {
|
|
47175
|
-
return lens(lastSerializedState).get(path);
|
|
47176
|
-
}
|
|
47177
|
-
return null;
|
|
47178
|
-
});
|
|
47179
|
-
}
|
|
46853
|
+
/* eslint-disable eqeqeq, @typescript-eslint/no-unused-vars */
|
|
47180
46854
|
|
|
47181
|
-
|
|
47182
|
-
|
|
47183
|
-
|
|
47184
|
-
|
|
47185
|
-
|
|
47186
|
-
|
|
47187
|
-
|
|
47188
|
-
|
|
47189
|
-
|
|
47190
|
-
|
|
47191
|
-
|
|
47192
|
-
|
|
47193
|
-
|
|
47194
|
-
|
|
46855
|
+
/**
|
|
46856
|
+
* Format a floating point for display using the specified
|
|
46857
|
+
* number of significant figures and least significant decimal.
|
|
46858
|
+
* Scientific notation may used by this method if this
|
|
46859
|
+
* object is very small, very large, has many significant
|
|
46860
|
+
* figures, or the number of significant figures would be
|
|
46861
|
+
* ambiguous in the output if scientific notation were not
|
|
46862
|
+
* used.
|
|
46863
|
+
*
|
|
46864
|
+
* @param f A floating point number that should be displayed
|
|
46865
|
+
* @param sigFigs desired number of significant figures (integer).
|
|
46866
|
+
* @param sigDecs the least significant decimal place (integer).
|
|
46867
|
+
* @param scientific true iff scientific notation should always be used.
|
|
46868
|
+
* @return a string of this object formatted correctly.
|
|
46869
|
+
*/
|
|
46870
|
+
function displaySigFigs(f, sigFigs, sigDecs, scientific) {
|
|
46871
|
+
const s = "" + f;
|
|
46872
|
+
let order = parseOrder(s);
|
|
46873
|
+
let mantissa = parseMantissa(s);
|
|
46874
|
+
let positive = parseSign(s);
|
|
46875
|
+
let add;
|
|
46876
|
+
let decAdd;
|
|
46877
|
+
let sigAdd;
|
|
46878
|
+
let zeroScientific = false;
|
|
46879
|
+
if (f == 0 || mantissa == "" || mantissa == "0") {
|
|
46880
|
+
mantissa = "";
|
|
46881
|
+
for (let i = 0; i < sigFigs; i++) {
|
|
46882
|
+
mantissa += "0";
|
|
46883
|
+
}
|
|
46884
|
+
order = sigFigs + sigDecs;
|
|
46885
|
+
if (sigDecs < 0 && -sigDecs >= sigFigs) {
|
|
46886
|
+
zeroScientific = true;
|
|
46887
|
+
}
|
|
46888
|
+
} else {
|
|
46889
|
+
decAdd = order - mantissa.length - sigDecs;
|
|
46890
|
+
sigAdd = sigFigs - mantissa.length;
|
|
46891
|
+
add = Math.min(sigAdd, decAdd);
|
|
46892
|
+
if (add < 0) {
|
|
46893
|
+
let rounded = round(mantissa, -add);
|
|
46894
|
+
if (rounded.length > mantissa.length + add) {
|
|
46895
|
+
order++;
|
|
46896
|
+
if (decAdd > sigAdd) {
|
|
46897
|
+
rounded = round(rounded, 1);
|
|
46898
|
+
}
|
|
47195
46899
|
}
|
|
47196
|
-
|
|
47197
|
-
|
|
47198
|
-
|
|
47199
|
-
|
|
46900
|
+
mantissa = rounded;
|
|
46901
|
+
} else if (add > 0) {
|
|
46902
|
+
for (let i = 0; i < add; i++) {
|
|
46903
|
+
mantissa += "0";
|
|
47200
46904
|
}
|
|
47201
|
-
|
|
47202
|
-
|
|
47203
|
-
|
|
46905
|
+
}
|
|
46906
|
+
if (mantissa == "" || mantissa == "0") {
|
|
46907
|
+
mantissa = "0";
|
|
46908
|
+
positive = true;
|
|
46909
|
+
order = 1 + sigDecs;
|
|
46910
|
+
if (order != 0) {
|
|
46911
|
+
zeroScientific = true;
|
|
47204
46912
|
}
|
|
47205
|
-
numCallbacks++;
|
|
47206
|
-
data.ref?.restoreSerializedState(state, countCallback);
|
|
47207
|
-
});
|
|
47208
|
-
}
|
|
47209
|
-
|
|
47210
|
-
/**
|
|
47211
|
-
* Given an array of renderers, if it happens to be an array of *hint*
|
|
47212
|
-
* renderers, then attach a `firstN` method to the array, which allows the
|
|
47213
|
-
* layout to render the hints together in one HintsRenderer.
|
|
47214
|
-
*/
|
|
47215
|
-
_annotateRendererArray(renderers, rendererDatas, shape) {
|
|
47216
|
-
if (shape.elementShape.type === "hint") {
|
|
47217
|
-
// The shape says that these are HintRendererDatas, even though
|
|
47218
|
-
// it's not provable at compile time, so perform a cast.
|
|
47219
|
-
const hintRendererDatas = rendererDatas;
|
|
47220
|
-
renderers = [...renderers];
|
|
47221
|
-
renderers.firstN = n => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
|
|
47222
|
-
findExternalWidgets: hintRendererDatas[0] ? hintRendererDatas[0].findExternalWidgets : undefined,
|
|
47223
|
-
hints: hintRendererDatas.map(d => d.hint),
|
|
47224
|
-
hintsVisible: n
|
|
47225
|
-
}));
|
|
47226
46913
|
}
|
|
47227
|
-
return renderers;
|
|
47228
46914
|
}
|
|
47229
|
-
|
|
47230
|
-
|
|
47231
|
-
|
|
47232
|
-
|
|
47233
|
-
*
|
|
47234
|
-
* This is generated by running each of the `makeRenderer` functions at the
|
|
47235
|
-
* leaf nodes.
|
|
47236
|
-
*/
|
|
47237
|
-
_getRenderers() {
|
|
47238
|
-
return this.getRenderersMapper.mapTree(this.state.rendererDataTree, this.props.shape);
|
|
46915
|
+
const useScientific = scientific || mantissa.length > 20 || order > 4 || order < -2 || order - mantissa.length > 0 && trailingZeros(mantissa) > 0 || zeroScientific;
|
|
46916
|
+
let returnVal = "";
|
|
46917
|
+
if (!positive) {
|
|
46918
|
+
returnVal += "-";
|
|
47239
46919
|
}
|
|
47240
|
-
|
|
47241
|
-
|
|
47242
|
-
|
|
47243
|
-
|
|
47244
|
-
}, this.context.strings.errorRendering({
|
|
47245
|
-
error: String(this.state.renderError)
|
|
47246
|
-
}));
|
|
46920
|
+
if (useScientific) {
|
|
46921
|
+
returnVal += mantissa.charAt(0);
|
|
46922
|
+
if (mantissa.length > 1) {
|
|
46923
|
+
returnVal += "." + mantissa.substring(1, mantissa.length);
|
|
47247
46924
|
}
|
|
47248
|
-
|
|
47249
|
-
|
|
47250
|
-
|
|
47251
|
-
|
|
47252
|
-
|
|
47253
|
-
|
|
47254
|
-
|
|
47255
|
-
|
|
46925
|
+
if (order - 1 != 0) {
|
|
46926
|
+
returnVal += " x 10^" + (order - 1);
|
|
46927
|
+
}
|
|
46928
|
+
} else {
|
|
46929
|
+
let wholePart = "";
|
|
46930
|
+
let fractPart = "";
|
|
46931
|
+
let needDot = true;
|
|
46932
|
+
if (order > 0) {
|
|
46933
|
+
if (mantissa.length > order) {
|
|
46934
|
+
wholePart = mantissa.substring(0, order);
|
|
46935
|
+
fractPart = mantissa.substring(order, mantissa.length);
|
|
46936
|
+
} else {
|
|
46937
|
+
wholePart = mantissa;
|
|
46938
|
+
needDot = trailingZeros(mantissa) != 0;
|
|
46939
|
+
for (let i = 0; i < order - mantissa.length; i++) {
|
|
46940
|
+
wholePart += "0";
|
|
46941
|
+
}
|
|
46942
|
+
}
|
|
46943
|
+
} else {
|
|
46944
|
+
for (let i = 0; i < -order; i++) {
|
|
46945
|
+
fractPart += "0";
|
|
46946
|
+
}
|
|
46947
|
+
fractPart += mantissa;
|
|
46948
|
+
}
|
|
46949
|
+
returnVal += (wholePart == "" ? "0" : wholePart) + (needDot ? "." : "") + fractPart;
|
|
47256
46950
|
}
|
|
46951
|
+
return returnVal;
|
|
47257
46952
|
}
|
|
47258
|
-
const styles = aphrodite.StyleSheet.create({
|
|
47259
|
-
error: {
|
|
47260
|
-
color: "red"
|
|
47261
|
-
}
|
|
47262
|
-
});
|
|
47263
46953
|
|
|
47264
46954
|
/**
|
|
47265
|
-
*
|
|
46955
|
+
* Count the significant trailing zeros on this object.
|
|
47266
46956
|
*
|
|
47267
|
-
*
|
|
47268
|
-
* types instead.
|
|
46957
|
+
* @return the number of trailing zeros
|
|
47269
46958
|
*/
|
|
46959
|
+
function trailingZeros(mantissa) {
|
|
46960
|
+
let zeros = 0;
|
|
46961
|
+
for (let i = mantissa.length - 1; i >= 0; i--) {
|
|
46962
|
+
const c = mantissa.charAt(i);
|
|
46963
|
+
if (c == "0") {
|
|
46964
|
+
zeros++;
|
|
46965
|
+
} else {
|
|
46966
|
+
return zeros;
|
|
46967
|
+
}
|
|
46968
|
+
}
|
|
46969
|
+
return zeros;
|
|
46970
|
+
}
|
|
47270
46971
|
|
|
47271
46972
|
/**
|
|
47272
|
-
*
|
|
47273
|
-
*
|
|
46973
|
+
* Parse a string representation of a floating point
|
|
46974
|
+
* and pull out the sign.
|
|
47274
46975
|
*
|
|
47275
|
-
*
|
|
46976
|
+
* @param s the string representation of a floating point.
|
|
46977
|
+
* @return true iff this is a positive number
|
|
47276
46978
|
*/
|
|
47277
|
-
function
|
|
47278
|
-
|
|
47279
|
-
|
|
47280
|
-
|
|
46979
|
+
function parseSign(s) {
|
|
46980
|
+
let beginning = true;
|
|
46981
|
+
let seenDot = false;
|
|
46982
|
+
let seenSomething = false;
|
|
46983
|
+
let zeros = "";
|
|
46984
|
+
let all = "";
|
|
46985
|
+
let pos = true;
|
|
46986
|
+
for (let i = 0; i < s.length; i++) {
|
|
46987
|
+
const c = s.charAt(i);
|
|
46988
|
+
if (c >= "1" && c <= "9") {
|
|
46989
|
+
all += zeros + c;
|
|
46990
|
+
zeros = "";
|
|
46991
|
+
seenSomething = true;
|
|
46992
|
+
beginning = false;
|
|
46993
|
+
} else if (c == "0") {
|
|
46994
|
+
if (seenDot) {
|
|
46995
|
+
if (seenSomething) {
|
|
46996
|
+
all += zeros + c;
|
|
46997
|
+
zeros = "";
|
|
46998
|
+
}
|
|
46999
|
+
} else {
|
|
47000
|
+
if (seenSomething) {
|
|
47001
|
+
zeros += c;
|
|
47002
|
+
}
|
|
47003
|
+
}
|
|
47004
|
+
beginning = false;
|
|
47005
|
+
} else if (!seenDot && c == ".") {
|
|
47006
|
+
all += zeros;
|
|
47007
|
+
zeros = "";
|
|
47008
|
+
seenDot = true;
|
|
47009
|
+
beginning = false;
|
|
47010
|
+
} else if (c == "e" || c == "E" && i + 1 < s.length) {
|
|
47011
|
+
parseInt(s.substring(i + 1, s.length));
|
|
47012
|
+
i = s.length;
|
|
47013
|
+
} else if (beginning && (c == "+" || c == "-")) {
|
|
47014
|
+
if (c == "-") {
|
|
47015
|
+
pos = !pos;
|
|
47016
|
+
}
|
|
47017
|
+
}
|
|
47018
|
+
}
|
|
47019
|
+
if (all == "") {
|
|
47020
|
+
return true;
|
|
47021
|
+
}
|
|
47022
|
+
return pos;
|
|
47281
47023
|
}
|
|
47282
47024
|
|
|
47283
47025
|
/**
|
|
47284
|
-
*
|
|
47285
|
-
*
|
|
47026
|
+
* Parse a string representation of a floating point
|
|
47027
|
+
* and pull out the mantissa.
|
|
47028
|
+
*
|
|
47029
|
+
* @param s the string representation of a floating point.
|
|
47030
|
+
* @return the mantissa of this number.
|
|
47286
47031
|
*/
|
|
47287
|
-
function
|
|
47288
|
-
|
|
47289
|
-
|
|
47290
|
-
|
|
47291
|
-
|
|
47292
|
-
|
|
47293
|
-
|
|
47294
|
-
|
|
47295
|
-
|
|
47296
|
-
|
|
47297
|
-
|
|
47298
|
-
|
|
47299
|
-
|
|
47300
|
-
|
|
47301
|
-
|
|
47302
|
-
|
|
47303
|
-
|
|
47304
|
-
|
|
47032
|
+
function parseMantissa(s) {
|
|
47033
|
+
let seenDot = false;
|
|
47034
|
+
let seenSomething = false;
|
|
47035
|
+
let zeros = "";
|
|
47036
|
+
let leadZeros = "";
|
|
47037
|
+
let all = "";
|
|
47038
|
+
for (let i = 0; i < s.length; i++) {
|
|
47039
|
+
const c = s.charAt(i);
|
|
47040
|
+
if (c >= "1" && c <= "9") {
|
|
47041
|
+
all += zeros + c;
|
|
47042
|
+
zeros = "";
|
|
47043
|
+
seenSomething = true;
|
|
47044
|
+
} else if (c == "0") {
|
|
47045
|
+
if (seenDot) {
|
|
47046
|
+
if (seenSomething) {
|
|
47047
|
+
all += zeros + c;
|
|
47048
|
+
zeros = "";
|
|
47049
|
+
} else {
|
|
47050
|
+
leadZeros += c;
|
|
47051
|
+
}
|
|
47052
|
+
} else {
|
|
47053
|
+
if (seenSomething) {
|
|
47054
|
+
leadZeros += c;
|
|
47055
|
+
zeros += c;
|
|
47056
|
+
} else {
|
|
47057
|
+
leadZeros += c;
|
|
47058
|
+
}
|
|
47059
|
+
}
|
|
47060
|
+
} else if (!seenDot && c == ".") {
|
|
47061
|
+
all += zeros;
|
|
47062
|
+
zeros = "";
|
|
47063
|
+
seenDot = true;
|
|
47064
|
+
} else if (c == "e" || c == "E" && i + 1 < s.length) {
|
|
47065
|
+
parseInt(s.substring(i + 1, s.length));
|
|
47066
|
+
i = s.length;
|
|
47067
|
+
} else ;
|
|
47305
47068
|
}
|
|
47306
|
-
if (
|
|
47307
|
-
return
|
|
47069
|
+
if (all == "") {
|
|
47070
|
+
return leadZeros;
|
|
47308
47071
|
}
|
|
47309
|
-
|
|
47310
|
-
|
|
47311
|
-
|
|
47072
|
+
return all;
|
|
47073
|
+
}
|
|
47074
|
+
|
|
47075
|
+
/**
|
|
47076
|
+
* Parse a string representation of a floating point
|
|
47077
|
+
* and pull out the exponent.
|
|
47078
|
+
*
|
|
47079
|
+
* @param s the string representation of a floating point.
|
|
47080
|
+
* @return (integer) the number after the e.
|
|
47081
|
+
*/
|
|
47082
|
+
function parseOrder(s) {
|
|
47083
|
+
let seenDot = false;
|
|
47084
|
+
let seenSomething = false;
|
|
47085
|
+
let zeros = "";
|
|
47086
|
+
let all = "";
|
|
47087
|
+
let decPlaces = 0;
|
|
47088
|
+
let totalDecs = 0;
|
|
47089
|
+
for (let i = 0; i < s.length; i++) {
|
|
47090
|
+
const c = s.charAt(i);
|
|
47091
|
+
if (c >= "1" && c <= "9") {
|
|
47092
|
+
all += zeros + c;
|
|
47093
|
+
zeros = "";
|
|
47094
|
+
seenSomething = true;
|
|
47095
|
+
if (!seenDot) {
|
|
47096
|
+
totalDecs++;
|
|
47097
|
+
decPlaces++;
|
|
47098
|
+
}
|
|
47099
|
+
} else if (c == "0") {
|
|
47100
|
+
if (seenDot) {
|
|
47101
|
+
if (seenSomething) {
|
|
47102
|
+
all += zeros + c;
|
|
47103
|
+
zeros = "";
|
|
47104
|
+
} else {
|
|
47105
|
+
decPlaces--;
|
|
47106
|
+
}
|
|
47107
|
+
} else {
|
|
47108
|
+
totalDecs++;
|
|
47109
|
+
if (seenSomething) {
|
|
47110
|
+
decPlaces++;
|
|
47111
|
+
zeros += c;
|
|
47112
|
+
}
|
|
47113
|
+
}
|
|
47114
|
+
} else if (!seenDot && c == ".") {
|
|
47115
|
+
all += zeros;
|
|
47116
|
+
zeros = "";
|
|
47117
|
+
seenDot = true;
|
|
47118
|
+
} else if (c == "e" || c == "E" && i + 1 < s.length) {
|
|
47119
|
+
const raised = parseInt(s.substring(i + 1, s.length));
|
|
47120
|
+
decPlaces += raised;
|
|
47121
|
+
totalDecs += raised;
|
|
47122
|
+
i = s.length;
|
|
47123
|
+
} else ;
|
|
47312
47124
|
}
|
|
47313
|
-
if (
|
|
47314
|
-
|
|
47315
|
-
const propTypeShape = {};
|
|
47316
|
-
Object.keys(valueShapes).forEach(key => {
|
|
47317
|
-
propTypeShape[key] = buildTreePropTypeForShape(valueShapes[key]).isRequired;
|
|
47318
|
-
});
|
|
47319
|
-
return PropTypes__default["default"].shape(propTypeShape);
|
|
47125
|
+
if (all == "") {
|
|
47126
|
+
return totalDecs;
|
|
47320
47127
|
}
|
|
47321
|
-
|
|
47322
|
-
// @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
|
|
47323
|
-
`unexpected shape type ${shape.type}`, perseusCore.Errors.InvalidInput);
|
|
47128
|
+
return decPlaces;
|
|
47324
47129
|
}
|
|
47325
47130
|
|
|
47326
47131
|
/**
|
|
47327
|
-
*
|
|
47328
|
-
*
|
|
47329
|
-
*
|
|
47330
|
-
*
|
|
47331
|
-
*
|
|
47332
|
-
*
|
|
47333
|
-
*
|
|
47334
|
-
*
|
|
47335
|
-
* multi-rendering. To multi-render a question, pass in the content of the item
|
|
47336
|
-
* to the `MultiRenderer` component as a props. Then, pass in a function which
|
|
47337
|
-
* takes an object of renderers (in the same structure as the content), and
|
|
47338
|
-
* return a render tree. The `MultiRenderer` component will allow you to
|
|
47339
|
-
* combine scores, serialized state, etc. without having to manually call on
|
|
47340
|
-
* each of the functions. It also handles inter-widgets requests between the
|
|
47341
|
-
* different renderers.
|
|
47342
|
-
* For more details, see `multi-items/multi-renderer.jsx`.
|
|
47343
|
-
*
|
|
47344
|
-
* Example:
|
|
47345
|
-
*
|
|
47346
|
-
* item = {_multi: {
|
|
47347
|
-
* left: <content data>,
|
|
47348
|
-
* right: [<content data>, <content data>],
|
|
47349
|
-
* }}
|
|
47350
|
-
* shape = shapes.shape({
|
|
47351
|
-
* left: shapes.content,
|
|
47352
|
-
* right: shapes.arrayOf(shapes.content),
|
|
47353
|
-
* })
|
|
47354
|
-
*
|
|
47355
|
-
* <MultiRenderer item={item} shape={shape}>
|
|
47356
|
-
* {({renderers}) =>
|
|
47357
|
-
* <div>
|
|
47358
|
-
* <div id="left">{renderers.left}</div>
|
|
47359
|
-
* <ul id="right">
|
|
47360
|
-
* {renderers.right.map(r => <li>{r}</li>)}
|
|
47361
|
-
* </ul>
|
|
47362
|
-
* </div>
|
|
47363
|
-
* }
|
|
47364
|
-
* </MultiRenderer>
|
|
47365
|
-
*
|
|
47366
|
-
* This file also exposes `shapes`, which helps you construct a runtime type
|
|
47367
|
-
* declaration for your particular class of multi-item. This can then be used
|
|
47368
|
-
* to create a MultirendererEditor for your multi-item shape, and to validate
|
|
47369
|
-
* that a multi-item conforms to the shape via `buildPropTypeForShape`.
|
|
47370
|
-
* For more details, see `multi-items/shapes.js`.
|
|
47371
|
-
*
|
|
47372
|
-
* This file also exposes some utility functions for working with generic
|
|
47373
|
-
* multi-items, like `findContentNodesInItem`, `findHintNodesInItem`,
|
|
47374
|
-
* `inferItemShape`, and `buildEmptyItemForShape`.
|
|
47375
|
-
* For more details, see `multi-items/items.js`.
|
|
47376
|
-
*/
|
|
47377
|
-
var MultiItems = {
|
|
47378
|
-
// Tools for rendering your multi-items
|
|
47379
|
-
MultiRenderer,
|
|
47380
|
-
// Tools for declaring your multi-item shapes
|
|
47381
|
-
shapes,
|
|
47382
|
-
buildPropTypeForShape,
|
|
47383
|
-
// Tools for generically manipulating multi-items
|
|
47384
|
-
buildEmptyItemForShape,
|
|
47385
|
-
findContentNodesInItem: findContentNodesInItem$2,
|
|
47386
|
-
findHintNodesInItem,
|
|
47387
|
-
inferItemShape: inferItemShape$2
|
|
47388
|
-
};
|
|
47389
|
-
|
|
47390
|
-
/**
|
|
47391
|
-
* Calculates whether a perseus item is renderable by a specific
|
|
47392
|
-
* perseus-item-data version.
|
|
47132
|
+
* Remove the specified number of digits from string by
|
|
47133
|
+
* rounding. Proper rounding rules for scientific purposes
|
|
47134
|
+
* are followed.
|
|
47135
|
+
* This method may cause an extra significant figure
|
|
47136
|
+
* to be added to the number. For example, if 999999
|
|
47137
|
+
* were rounded, A one would carry over and become
|
|
47138
|
+
* a significant figure. Those who call this method
|
|
47139
|
+
* should check for this and call round again if needed.
|
|
47393
47140
|
*
|
|
47394
|
-
*
|
|
47395
|
-
*
|
|
47396
|
-
*
|
|
47397
|
-
* group or sequence widgets.
|
|
47141
|
+
* @param mantissa A string representing an whole number of arbitrary length.
|
|
47142
|
+
* @param digits A number of digits to remove
|
|
47143
|
+
* @return A string represted the rounded version of mantissa
|
|
47398
47144
|
*/
|
|
47399
|
-
|
|
47400
|
-
|
|
47401
|
-
|
|
47402
|
-
|
|
47403
|
-
const isUpgradedWidgetInfoRenderableBy = function (widgetInfo, widgetRendererVersion) {
|
|
47404
|
-
if (widgetRendererVersion == null) {
|
|
47405
|
-
// If the widget does not exist in this version, this will
|
|
47406
|
-
// be null, and that version of perseus cannot render the
|
|
47407
|
-
// widget (it doesn't even know the widget exists!)
|
|
47408
|
-
return false;
|
|
47409
|
-
}
|
|
47410
|
-
const widgetVersion = widgetInfo.version || {
|
|
47411
|
-
major: 0,
|
|
47412
|
-
minor: 0
|
|
47413
|
-
};
|
|
47414
|
-
if (widgetRendererVersion.major > widgetVersion.major) {
|
|
47415
|
-
return true;
|
|
47416
|
-
}
|
|
47417
|
-
if (widgetRendererVersion.major < widgetVersion.major) {
|
|
47418
|
-
return false;
|
|
47419
|
-
}
|
|
47420
|
-
// If the major versions are the same, the minor version acts
|
|
47421
|
-
// like a tie-breaker.
|
|
47422
|
-
// For example, input-number 3.2 can render an input-number
|
|
47423
|
-
// 2.4, 3.0, or 3.2, but not an input number 3.3 or 4.0.
|
|
47424
|
-
return widgetRendererVersion.minor >= widgetVersion.minor;
|
|
47425
|
-
};
|
|
47426
|
-
const isRawWidgetInfoRenderableBy = function (widgetInfo, rendererContentVersion) {
|
|
47427
|
-
// Empty/non-existant widgets are always safe to render
|
|
47428
|
-
if (widgetInfo == null || widgetInfo.type == null) {
|
|
47429
|
-
return true;
|
|
47145
|
+
function round(mantissa, digits) {
|
|
47146
|
+
const last = mantissa.length - digits - 1;
|
|
47147
|
+
if (last < 0) {
|
|
47148
|
+
return "";
|
|
47430
47149
|
}
|
|
47431
|
-
|
|
47432
|
-
|
|
47433
|
-
// is at a later version than is supported.
|
|
47434
|
-
const upgradedWidgetInfo = upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
47435
|
-
return isUpgradedWidgetInfoRenderableBy(upgradedWidgetInfo, rendererContentVersion[upgradedWidgetInfo.type]);
|
|
47436
|
-
};
|
|
47437
|
-
const isRendererContentRenderableBy = function (rendererOptions, rendererContentVersion) {
|
|
47438
|
-
let isRenderable = true;
|
|
47439
|
-
traverse(rendererOptions, null, function (widgetInfo) {
|
|
47440
|
-
isRenderable = isRenderable && isRawWidgetInfoRenderableBy(widgetInfo, rendererContentVersion);
|
|
47441
|
-
});
|
|
47442
|
-
return isRenderable;
|
|
47443
|
-
};
|
|
47444
|
-
const isItemRenderableByVersion = function (itemData, rendererContentVersion) {
|
|
47445
|
-
if (itemData == null || rendererContentVersion == null) {
|
|
47446
|
-
throw new perseusCore.PerseusError("missing parameter to Perseus.isRenderable.item", perseusCore.Errors.InvalidInput);
|
|
47150
|
+
if (last >= mantissa.length - 1) {
|
|
47151
|
+
return mantissa;
|
|
47447
47152
|
}
|
|
47448
|
-
|
|
47449
|
-
|
|
47450
|
-
|
|
47451
|
-
|
|
47452
|
-
|
|
47453
|
-
|
|
47454
|
-
|
|
47153
|
+
const nextToLast = mantissa.charAt(last + 1);
|
|
47154
|
+
const lastChar = mantissa.charAt(last);
|
|
47155
|
+
let roundUp = false;
|
|
47156
|
+
if (nextToLast > "5") {
|
|
47157
|
+
roundUp = true;
|
|
47158
|
+
} else if (nextToLast == "5") {
|
|
47159
|
+
for (let j = last + 2; j < mantissa.length; j++) {
|
|
47160
|
+
if (mantissa.charAt(j) != "0") {
|
|
47161
|
+
roundUp = true;
|
|
47455
47162
|
}
|
|
47456
|
-
}
|
|
47457
|
-
|
|
47163
|
+
}
|
|
47164
|
+
// @ts-expect-error - TS2362 - The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
|
|
47165
|
+
if (lastChar % 2 == 1) {
|
|
47166
|
+
roundUp = true;
|
|
47167
|
+
}
|
|
47458
47168
|
}
|
|
47459
|
-
|
|
47460
|
-
|
|
47461
|
-
|
|
47462
|
-
|
|
47463
|
-
|
|
47464
|
-
|
|
47465
|
-
|
|
47466
|
-
|
|
47467
|
-
|
|
47468
|
-
|
|
47469
|
-
|
|
47470
|
-
|
|
47471
|
-
|
|
47472
|
-
|
|
47473
|
-
|
|
47474
|
-
|
|
47475
|
-
|
|
47476
|
-
|
|
47169
|
+
let result = "";
|
|
47170
|
+
for (let i = last; i >= 0; i--) {
|
|
47171
|
+
const c = mantissa.charAt(i);
|
|
47172
|
+
if (roundUp) {
|
|
47173
|
+
let nextChar;
|
|
47174
|
+
if (c == "9") {
|
|
47175
|
+
nextChar = "0";
|
|
47176
|
+
} else {
|
|
47177
|
+
switch (c) {
|
|
47178
|
+
case "0":
|
|
47179
|
+
nextChar = "1";
|
|
47180
|
+
break;
|
|
47181
|
+
case "1":
|
|
47182
|
+
nextChar = "2";
|
|
47183
|
+
break;
|
|
47184
|
+
case "2":
|
|
47185
|
+
nextChar = "3";
|
|
47186
|
+
break;
|
|
47187
|
+
case "3":
|
|
47188
|
+
nextChar = "4";
|
|
47189
|
+
break;
|
|
47190
|
+
case "4":
|
|
47191
|
+
nextChar = "5";
|
|
47192
|
+
break;
|
|
47193
|
+
case "5":
|
|
47194
|
+
nextChar = "6";
|
|
47195
|
+
break;
|
|
47196
|
+
case "6":
|
|
47197
|
+
nextChar = "7";
|
|
47198
|
+
break;
|
|
47199
|
+
case "7":
|
|
47200
|
+
nextChar = "8";
|
|
47201
|
+
break;
|
|
47202
|
+
case "8":
|
|
47203
|
+
nextChar = "9";
|
|
47204
|
+
break;
|
|
47205
|
+
}
|
|
47206
|
+
roundUp = false;
|
|
47207
|
+
}
|
|
47208
|
+
result = nextChar + result;
|
|
47209
|
+
} else {
|
|
47210
|
+
result = c + result;
|
|
47477
47211
|
}
|
|
47478
|
-
});
|
|
47479
|
-
}
|
|
47480
|
-
|
|
47481
|
-
// Returns a list of widgets that cause a given perseus item to require
|
|
47482
|
-
// the use of a screen or mouse.
|
|
47483
|
-
//
|
|
47484
|
-
// For now we'll just check the `accessible` field on each of the widgets
|
|
47485
|
-
// in the item data, but in the future we may specify accessibility on
|
|
47486
|
-
// each widget with higher granularity.
|
|
47487
|
-
function violatingWidgets(itemData) {
|
|
47488
|
-
// TODO(jordan): Hints as well
|
|
47489
|
-
const widgets = [];
|
|
47490
|
-
if (itemData._multi) {
|
|
47491
|
-
const shape = inferItemShape(itemData);
|
|
47492
|
-
findContentNodesInItem(itemData, shape, content => traverseRenderer(content, widgets));
|
|
47493
|
-
} else {
|
|
47494
|
-
traverseRenderer(itemData.question, widgets);
|
|
47495
47212
|
}
|
|
47496
|
-
|
|
47497
|
-
|
|
47498
|
-
|
|
47213
|
+
if (roundUp) {
|
|
47214
|
+
result = "1" + result;
|
|
47215
|
+
}
|
|
47216
|
+
return result;
|
|
47499
47217
|
}
|
|
47500
47218
|
|
|
47501
47219
|
/**
|
|
@@ -48219,9 +47937,6 @@ function generateTestInteractiveGraphWidget() {
|
|
|
48219
47937
|
markings: "graph",
|
|
48220
47938
|
labels: ["x", "y"],
|
|
48221
47939
|
showProtractor: false,
|
|
48222
|
-
showRuler: false,
|
|
48223
|
-
rulerLabel: "mm",
|
|
48224
|
-
rulerTicks: 2,
|
|
48225
47940
|
range: [[-10, 10], [-10, 10]],
|
|
48226
47941
|
graph: {
|
|
48227
47942
|
type: "angle"
|
|
@@ -48333,7 +48048,6 @@ const widgetNameToEnum = {
|
|
|
48333
48048
|
simulator: "SIMULATOR",
|
|
48334
48049
|
sorter: "SORTER",
|
|
48335
48050
|
table: "TABLE",
|
|
48336
|
-
"unit-input": "UNIT",
|
|
48337
48051
|
video: "VIDEO"
|
|
48338
48052
|
};
|
|
48339
48053
|
function convertWidgetNameToEnum(name) {
|
|
@@ -48415,6 +48129,8 @@ exports.getAnswerFromUserInput = getAnswerFromUserInput;
|
|
|
48415
48129
|
exports.getAnswersFromWidgets = getAnswersFromWidgets;
|
|
48416
48130
|
exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
|
|
48417
48131
|
exports.getLineCoords = getLineCoords;
|
|
48132
|
+
exports.getLinearSystemCoords = getLinearSystemCoords;
|
|
48133
|
+
exports.getSegmentCoords = getSegmentCoords;
|
|
48418
48134
|
exports.getValidWidgetIds = getValidWidgetIds;
|
|
48419
48135
|
exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
|
|
48420
48136
|
exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
|