@khanacademy/perseus 19.0.0 → 19.1.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/CHANGELOG.md +37 -0
- package/dist/es/index.css +209 -104
- package/dist/es/index.js +806 -877
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +209 -104
- package/dist/index.d.ts +3 -2
- package/dist/index.js +849 -920
- package/dist/index.js.map +1 -1
- package/dist/mixins/changeable.d.ts +1 -1
- package/dist/perseus-api.d.ts +1 -0
- package/dist/renderer.d.ts +2 -4
- package/dist/server-item-renderer.d.ts +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/util/geometry.d.ts +2 -0
- package/dist/util/graphie.d.ts +107 -1
- package/dist/util/math.d.ts +2 -0
- package/dist/util/parse-perseus-json.d.ts +3 -3
- package/dist/widgets/expression.d.ts +6 -0
- package/dist/widgets/label-image.d.ts +2 -0
- package/package.json +3 -4
- package/src/__testdata__/renderer.testdata.ts +152 -0
- package/src/__tests__/renderer.test.tsx +17 -0
- package/src/components/math-input.tsx +4 -6
- package/src/index.ts +8 -3
- package/src/mixins/changeable.ts +1 -1
- package/src/renderer.tsx +14 -8
- package/src/types.ts +9 -0
- package/src/util/geometry.test.ts +75 -0
- package/src/util/geometry.ts +19 -0
- package/src/util/graphie.test.ts +1291 -0
- package/src/util/graphie.ts +832 -784
- package/src/util/interactive.ts +23 -51
- package/src/util/math.test.ts +11 -0
- package/src/util/math.ts +8 -0
- package/src/util/parse-perseus-json.test.ts +3 -3
- package/src/util/parse-perseus-json.ts +4 -4
- package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +4 -2
- package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +4 -2
- package/src/widgets/__tests__/expression-mobile.test.tsx +8 -5
- package/src/widgets/expression.tsx +1 -23
- package/src/widgets/label-image.tsx +0 -1
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -26,5 +26,5 @@ export declare const propTypes: {
|
|
|
26
26
|
export type ChangeableProps = {
|
|
27
27
|
onChange: (values: {
|
|
28
28
|
[key: string]: any;
|
|
29
|
-
}, callback?: () => unknown | null | undefined, silent?: boolean) => unknown;
|
|
29
|
+
}, callback?: (() => unknown) | null | undefined, silent?: boolean) => unknown;
|
|
30
30
|
};
|
package/dist/perseus-api.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ export declare const ApiOptions: {
|
|
|
93
93
|
inModal?: boolean | undefined;
|
|
94
94
|
crossOutEnabled?: boolean | undefined;
|
|
95
95
|
editorChangeDelay?: number | undefined;
|
|
96
|
+
onWidgetStartProps?: ((widgets: import("./types").PerseusWidgetMap) => import("./types").PerseusWidgetMap) | undefined;
|
|
96
97
|
}> & {
|
|
97
98
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
98
99
|
baseElements: {
|
package/dist/renderer.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import TranslationLinter from "./translation-linter";
|
|
|
4
4
|
import WidgetContainer from "./widget-container";
|
|
5
5
|
import type { DependenciesContext } from "./dependencies";
|
|
6
6
|
import type { PerseusRenderer, PerseusWidget, PerseusWidgetOptions } from "./perseus-types";
|
|
7
|
-
import type { APIOptions, APIOptionsWithDefaults, FilterCriterion, FocusPath, PerseusScore, WidgetProps } from "./types";
|
|
7
|
+
import type { APIOptions, APIOptionsWithDefaults, FilterCriterion, FocusPath, PerseusScore, PerseusWidgetMap, WidgetProps } from "./types";
|
|
8
8
|
import type { LinterContextProps } from "@khanacademy/perseus-linter";
|
|
9
9
|
import "./styles/perseus-renderer.less";
|
|
10
10
|
type WidgetState = {
|
|
@@ -117,9 +117,7 @@ declare class Renderer extends React.Component<Props, State> {
|
|
|
117
117
|
_getAllWidgetsInfo: (props: Props) => {
|
|
118
118
|
[key: string]: PerseusWidget;
|
|
119
119
|
};
|
|
120
|
-
_getAllWidgetsStartProps: (allWidgetInfo:
|
|
121
|
-
[key: string]: PerseusWidget;
|
|
122
|
-
}, props: Props) => any;
|
|
120
|
+
_getAllWidgetsStartProps: (allWidgetInfo: PerseusWidgetMap, props: Props) => PerseusWidgetMap;
|
|
123
121
|
_getDefaultWidgetInfo: (widgetId: string) => any;
|
|
124
122
|
_getWidgetInfo: (widgetId: string) => PerseusWidget;
|
|
125
123
|
renderWidget: (impliedType: string, id: string, state: WidgetState) => null | React.ReactNode;
|
|
@@ -136,6 +136,7 @@ declare const ref: React.ForwardRefExoticComponent<Omit<Pick<OwnProps & HOCProps
|
|
|
136
136
|
inModal?: boolean | undefined;
|
|
137
137
|
crossOutEnabled?: boolean | undefined;
|
|
138
138
|
editorChangeDelay?: number | undefined;
|
|
139
|
+
onWidgetStartProps?: ((widgets: import("./types").PerseusWidgetMap) => import("./types").PerseusWidgetMap) | undefined;
|
|
139
140
|
}> | undefined;
|
|
140
141
|
onRendered?: ((isRendered: boolean) => void) | undefined;
|
|
141
142
|
} & {}, "onRendered"> & React.RefAttributes<ServerItemRenderer>>;
|
package/dist/types.d.ts
CHANGED
|
@@ -103,6 +103,7 @@ export type APIOptions = Readonly<{
|
|
|
103
103
|
inModal?: boolean;
|
|
104
104
|
crossOutEnabled?: boolean;
|
|
105
105
|
editorChangeDelay?: number;
|
|
106
|
+
onWidgetStartProps?: (widgets: PerseusWidgetMap) => PerseusWidgetMap;
|
|
106
107
|
}>;
|
|
107
108
|
type TeXProps = {
|
|
108
109
|
children: string;
|
|
@@ -242,6 +243,9 @@ export type WidgetExports<T extends React.ComponentType<any> = React.ComponentTy
|
|
|
242
243
|
};
|
|
243
244
|
widget: T;
|
|
244
245
|
}>;
|
|
246
|
+
export type PerseusWidgetMap = {
|
|
247
|
+
[key: string]: PerseusWidget;
|
|
248
|
+
};
|
|
245
249
|
export type FilterCriterion = string | ((id: string, widgetInfo: PerseusWidget, widget?: Widget | null | undefined) => boolean);
|
|
246
250
|
export type WidgetProps<RenderProps, Rubric, TrackingExtraArgs = Empty> = RenderProps & {
|
|
247
251
|
widgetId: string;
|
package/dist/util/geometry.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare function ccw(a: Coord, b: Coord, c: Coord): number;
|
|
|
15
15
|
export declare function collinear(a: Coord, b: Coord, c: Coord): boolean;
|
|
16
16
|
export declare function intersects(ab: Line, cd: Line): boolean;
|
|
17
17
|
export declare function vector(a: any, b: any): number[];
|
|
18
|
+
export declare function reverseVector(vector: Coord): Coord;
|
|
19
|
+
export declare function clockwise(points: Coord[]): boolean;
|
|
18
20
|
export declare function magnitude(v: ReadonlyArray<Coord>): number;
|
|
19
21
|
export declare function angleMeasures(coords: ReadonlyArray<Coord>): ReadonlyArray<number>;
|
|
20
22
|
export declare function similar(coords1: ReadonlyArray<Coord>, coords2: ReadonlyArray<Coord>, tolerance: number): boolean;
|
package/dist/util/graphie.d.ts
CHANGED
|
@@ -1,4 +1,110 @@
|
|
|
1
|
+
import type { Interval } from "./interval";
|
|
1
2
|
import type { Coord } from "../interactive2/types";
|
|
2
|
-
export declare function polar(r: number | Coord, th: number):
|
|
3
|
+
export declare function polar(r: number | Coord, th: number): Coord;
|
|
4
|
+
interface RaphaelElement {
|
|
5
|
+
type: string;
|
|
6
|
+
attrs: Record<string, any>;
|
|
7
|
+
node: {
|
|
8
|
+
style: {
|
|
9
|
+
shapeRendering: "crispEdges" | "geometricPrecision";
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
type PositionedShape = {
|
|
14
|
+
wrapper: HTMLDivElement;
|
|
15
|
+
visibleShape: RaphaelElement;
|
|
16
|
+
};
|
|
17
|
+
export type StyleParams = {
|
|
18
|
+
fill?: string;
|
|
19
|
+
labelDistance?: number;
|
|
20
|
+
opacity?: number;
|
|
21
|
+
step?: Coord;
|
|
22
|
+
stroke?: string;
|
|
23
|
+
strokeWidth?: number;
|
|
24
|
+
};
|
|
25
|
+
type LabelPosition = "center" | "above" | "below" | "right" | "left" | "above right" | "above left" | "below right" | "below left";
|
|
26
|
+
interface LabelMethod {
|
|
27
|
+
(point: Coord, text: string, position: LabelPosition): any;
|
|
28
|
+
(point: Coord, text: string, position: LabelPosition, renderTex: boolean): any;
|
|
29
|
+
(point: Coord, text: string, position: LabelPosition, style: StyleParams): any;
|
|
30
|
+
(point: Coord, text: string, position: LabelPosition, renderTex: boolean, style: StyleParams): any;
|
|
31
|
+
}
|
|
32
|
+
export declare class Graphie {
|
|
33
|
+
#private;
|
|
34
|
+
el: HTMLElement;
|
|
35
|
+
raphael?: any;
|
|
36
|
+
isMobile: boolean;
|
|
37
|
+
currentStyle: any;
|
|
38
|
+
range?: [Interval, Interval];
|
|
39
|
+
scale?: Coord;
|
|
40
|
+
dimensions?: Coord;
|
|
41
|
+
xpixels?: number;
|
|
42
|
+
ypixels?: number;
|
|
43
|
+
constructor(el: HTMLElement);
|
|
44
|
+
init(options: {
|
|
45
|
+
range?: [Interval, Interval];
|
|
46
|
+
scale?: number | Coord;
|
|
47
|
+
isMobile?: boolean;
|
|
48
|
+
}): this;
|
|
49
|
+
graphInit(options: {
|
|
50
|
+
range?: [Interval, Interval] | Coord;
|
|
51
|
+
grid?: boolean;
|
|
52
|
+
gridRange?: [Interval, Interval] | Coord;
|
|
53
|
+
scale: Coord | number;
|
|
54
|
+
axes?: true;
|
|
55
|
+
axisArrows?: "<->" | "->" | true | "";
|
|
56
|
+
axisOpacity?: number;
|
|
57
|
+
axisCenter?: Coord;
|
|
58
|
+
axisLabels?: [string, string] | false;
|
|
59
|
+
gridOpacity?: number;
|
|
60
|
+
gridStep?: Coord | number;
|
|
61
|
+
ticks?: boolean;
|
|
62
|
+
tickStep?: Coord | number;
|
|
63
|
+
tickLen?: Coord | number;
|
|
64
|
+
tickOpacity?: number;
|
|
65
|
+
labels?: boolean;
|
|
66
|
+
labelStep?: number;
|
|
67
|
+
labelOpacity?: number;
|
|
68
|
+
labelFormat?: (a: number) => string;
|
|
69
|
+
yLabelFormat?: (y: number) => string;
|
|
70
|
+
xLabelFormat?: (x: number) => string;
|
|
71
|
+
unityLabels?: boolean | [boolean, boolean];
|
|
72
|
+
isMobile?: boolean;
|
|
73
|
+
}): void;
|
|
74
|
+
private drawingTransform;
|
|
75
|
+
private bounds;
|
|
76
|
+
style(attrs: any, fn: any): any;
|
|
77
|
+
grid(xr: Interval, yr: Interval, style?: StyleParams): unknown;
|
|
78
|
+
arc(center: Coord, radius: Coord, startAngle: number, endAngle: number, sector: boolean, style?: StyleParams): RaphaelElement;
|
|
79
|
+
circle(center: Coord, radius: number, style?: StyleParams): any;
|
|
80
|
+
rect(x: any, y: any, width: any, height: any, style?: StyleParams): any;
|
|
81
|
+
ellipse(center: any, radii: any, style?: StyleParams): any;
|
|
82
|
+
fixedEllipse(center: number | Coord, radii: number | [number, number], maxScale: number, padding: number, style?: StyleParams): PositionedShape;
|
|
83
|
+
private unstyledPath;
|
|
84
|
+
path(points: Coord[], style?: StyleParams): RaphaelElement;
|
|
85
|
+
fixedPath(points: Coord[], center: Coord | null, createPath: (scaledPoints: Coord[]) => string): PositionedShape;
|
|
86
|
+
scaledPath(points: Coord[], style?: StyleParams): RaphaelElement;
|
|
87
|
+
line(start: Coord, end: Coord, style?: StyleParams): RaphaelElement;
|
|
88
|
+
parabola(a: number, b: number, c: number, style?: StyleParams): RaphaelElement;
|
|
89
|
+
fixedLine(start: Coord, end: Coord, thickness: number): PositionedShape;
|
|
90
|
+
sinusoid(a: number, b: number, c: number, d: number, style?: StyleParams): RaphaelElement;
|
|
91
|
+
label: LabelMethod;
|
|
92
|
+
plotParametric(fn: (t: number) => Coord, range: Interval, style?: StyleParams): RaphaelElement;
|
|
93
|
+
plot(fn: (x: number) => number, range: Interval, style?: StyleParams): RaphaelElement;
|
|
94
|
+
svgPath: (points: (Coord | true)[], alreadyScaled?: boolean) => string;
|
|
95
|
+
svgParabolaPath: (a: any, b: any, c: any) => string;
|
|
96
|
+
svgSinusoidPath: (a: number, b: number, c: number, d: number) => string;
|
|
97
|
+
private withStyle;
|
|
98
|
+
private postprocessDrawingResult;
|
|
99
|
+
private addArrowheads;
|
|
100
|
+
scalePoint: (point: number | Coord) => Coord;
|
|
101
|
+
scaleVector: (point: number | Coord) => any;
|
|
102
|
+
unscalePoint: (point: Coord) => any;
|
|
103
|
+
unscaleVector: (point: Coord) => any;
|
|
104
|
+
private processAttributes;
|
|
105
|
+
}
|
|
106
|
+
type RangeSpecifier = [Interval, Interval] | Coord | number;
|
|
107
|
+
export declare function normalizeRange(range: RangeSpecifier): [Interval, Interval];
|
|
108
|
+
export declare function normalizeRange(range: RangeSpecifier | undefined): [Interval, Interval] | undefined;
|
|
3
109
|
declare const GraphUtils: any;
|
|
4
110
|
export default GraphUtils;
|
package/dist/util/math.d.ts
CHANGED
|
@@ -25,4 +25,6 @@ declare const KhanMath: {
|
|
|
25
25
|
readonly getNumericFormat: (text: string) => MathFormat | null | undefined;
|
|
26
26
|
readonly toNumericString: (number: number, format?: MathFormat) => string;
|
|
27
27
|
};
|
|
28
|
+
export declare function sum(array: number[]): number;
|
|
29
|
+
export declare function add(a: number, b: number): number;
|
|
28
30
|
export default KhanMath;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { PerseusItem } from "../perseus-types";
|
|
2
2
|
/**
|
|
3
|
-
* Helper to parse
|
|
3
|
+
* Helper to parse PerseusItem JSON
|
|
4
4
|
* Why not just use JSON.parse? We want:
|
|
5
5
|
* - To make sure types are correct
|
|
6
6
|
* - To give us a central place to validate/transform output if needed
|
|
7
|
-
* @param {string}
|
|
7
|
+
* @param {string} json - the stringified PerseusItem JSON
|
|
8
8
|
* @returns {PerseusItem} the parsed PerseusItem object
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export declare function parsePerseusItem(json: string): PerseusItem;
|
|
@@ -121,6 +121,7 @@ declare const _default: Readonly<{
|
|
|
121
121
|
inModal?: boolean | undefined;
|
|
122
122
|
crossOutEnabled?: boolean | undefined;
|
|
123
123
|
editorChangeDelay?: number | undefined;
|
|
124
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
124
125
|
}> & {
|
|
125
126
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
126
127
|
baseElements: {
|
|
@@ -184,6 +185,7 @@ declare const _default: Readonly<{
|
|
|
184
185
|
inModal?: boolean | undefined;
|
|
185
186
|
crossOutEnabled?: boolean | undefined;
|
|
186
187
|
editorChangeDelay?: number | undefined;
|
|
188
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
187
189
|
}> & {
|
|
188
190
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
189
191
|
baseElements: {
|
|
@@ -247,6 +249,7 @@ declare const _default: Readonly<{
|
|
|
247
249
|
inModal?: boolean | undefined;
|
|
248
250
|
crossOutEnabled?: boolean | undefined;
|
|
249
251
|
editorChangeDelay?: number | undefined;
|
|
252
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
250
253
|
}> & {
|
|
251
254
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
252
255
|
baseElements: {
|
|
@@ -321,6 +324,7 @@ declare const _default: Readonly<{
|
|
|
321
324
|
inModal?: boolean | undefined;
|
|
322
325
|
crossOutEnabled?: boolean | undefined;
|
|
323
326
|
editorChangeDelay?: number | undefined;
|
|
327
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
324
328
|
}> & {
|
|
325
329
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
326
330
|
baseElements: {
|
|
@@ -384,6 +388,7 @@ declare const _default: Readonly<{
|
|
|
384
388
|
inModal?: boolean | undefined;
|
|
385
389
|
crossOutEnabled?: boolean | undefined;
|
|
386
390
|
editorChangeDelay?: number | undefined;
|
|
391
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
387
392
|
}> & {
|
|
388
393
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
389
394
|
baseElements: {
|
|
@@ -447,6 +452,7 @@ declare const _default: Readonly<{
|
|
|
447
452
|
inModal?: boolean | undefined;
|
|
448
453
|
crossOutEnabled?: boolean | undefined;
|
|
449
454
|
editorChangeDelay?: number | undefined;
|
|
455
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
450
456
|
}> & {
|
|
451
457
|
GroupMetadataEditor: NonNullable<React.ComponentType<any> | undefined>;
|
|
452
458
|
baseElements: {
|
|
@@ -120,6 +120,7 @@ declare const _default: Readonly<{
|
|
|
120
120
|
inModal?: boolean | undefined;
|
|
121
121
|
crossOutEnabled?: boolean | undefined;
|
|
122
122
|
editorChangeDelay?: number | undefined;
|
|
123
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
123
124
|
}>;
|
|
124
125
|
choices: readonly string[];
|
|
125
126
|
imageAlt: string;
|
|
@@ -179,6 +180,7 @@ declare const _default: Readonly<{
|
|
|
179
180
|
inModal?: boolean | undefined;
|
|
180
181
|
crossOutEnabled?: boolean | undefined;
|
|
181
182
|
editorChangeDelay?: number | undefined;
|
|
183
|
+
onWidgetStartProps?: ((widgets: import("../types").PerseusWidgetMap) => import("../types").PerseusWidgetMap) | undefined;
|
|
182
184
|
}>;
|
|
183
185
|
choices: readonly string[];
|
|
184
186
|
imageAlt: string;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core Perseus API (includes renderers and widgets)",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "19.
|
|
6
|
+
"version": "19.1.1",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@khanacademy/kas": "^0.3.7",
|
|
26
|
-
"@khanacademy/kmath": "^0.1.
|
|
27
|
-
"@khanacademy/math-input": "^
|
|
26
|
+
"@khanacademy/kmath": "^0.1.9",
|
|
27
|
+
"@khanacademy/math-input": "^17.0.1",
|
|
28
28
|
"@khanacademy/perseus-core": "1.4.1",
|
|
29
29
|
"@khanacademy/perseus-linter": "^0.3.10",
|
|
30
30
|
"@khanacademy/pure-markdown": "^0.2.13",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
"jquery": "^2.1.1",
|
|
64
64
|
"katex": "^0.11.1",
|
|
65
65
|
"lodash.debounce": "^4.0.8",
|
|
66
|
-
"mathquill": "git+https://git@github.com/Khan/mathquill.git#48410e80d760bbd5105544d4a4ab459a28dc2cbc",
|
|
67
66
|
"perseus-build-settings": "^0.3.0",
|
|
68
67
|
"prop-types": "^15.6.1",
|
|
69
68
|
"react": "16.14.0",
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
InputNumberWidget,
|
|
5
5
|
PerseusRenderer,
|
|
6
6
|
} from "../perseus-types";
|
|
7
|
+
import type {RenderProps} from "../widgets/radio/widget";
|
|
7
8
|
|
|
8
9
|
export const dropdownWidget: DropdownWidget = {
|
|
9
10
|
type: "dropdown",
|
|
@@ -120,3 +121,154 @@ export const mockedItem: PerseusRenderer = {
|
|
|
120
121
|
},
|
|
121
122
|
},
|
|
122
123
|
} as PerseusRenderer;
|
|
124
|
+
|
|
125
|
+
// Note that if this item is used, you _must_ first register the MockWidget
|
|
126
|
+
export const mockedRandomItem: PerseusRenderer = {
|
|
127
|
+
content: "Mock widgets ==> [[\u2603 radio 1]] [[\u2603 radio 2]]",
|
|
128
|
+
images: {},
|
|
129
|
+
widgets: {
|
|
130
|
+
"radio 1": {
|
|
131
|
+
graded: true,
|
|
132
|
+
version: {major: 0, minor: 0},
|
|
133
|
+
static: false,
|
|
134
|
+
numCorrect: 1,
|
|
135
|
+
hasNoneOfTheAbove: false,
|
|
136
|
+
multipleSelect: false,
|
|
137
|
+
countChoices: false,
|
|
138
|
+
deselectEnabled: false,
|
|
139
|
+
type: "radio",
|
|
140
|
+
options: {
|
|
141
|
+
static: false,
|
|
142
|
+
countChoices: false,
|
|
143
|
+
deselectEnabled: false,
|
|
144
|
+
displayCount: null,
|
|
145
|
+
hasNoneOfTheAbove: false,
|
|
146
|
+
multipleSelect: false,
|
|
147
|
+
randomize: true,
|
|
148
|
+
choices: [
|
|
149
|
+
{
|
|
150
|
+
content: "Content 1",
|
|
151
|
+
correct: true,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
content: "Content 2",
|
|
155
|
+
correct: false,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
content: "Content 3",
|
|
159
|
+
correct: false,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
content: "Content 4",
|
|
163
|
+
correct: false,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
alignment: "default",
|
|
168
|
+
},
|
|
169
|
+
"radio 2": {
|
|
170
|
+
graded: true,
|
|
171
|
+
version: {major: 0, minor: 0},
|
|
172
|
+
static: false,
|
|
173
|
+
numCorrect: 1,
|
|
174
|
+
hasNoneOfTheAbove: false,
|
|
175
|
+
multipleSelect: false,
|
|
176
|
+
countChoices: false,
|
|
177
|
+
deselectEnabled: false,
|
|
178
|
+
type: "radio",
|
|
179
|
+
options: {
|
|
180
|
+
static: false,
|
|
181
|
+
countChoices: false,
|
|
182
|
+
deselectEnabled: false,
|
|
183
|
+
displayCount: null,
|
|
184
|
+
hasNoneOfTheAbove: false,
|
|
185
|
+
multipleSelect: false,
|
|
186
|
+
randomize: true,
|
|
187
|
+
choices: [
|
|
188
|
+
{
|
|
189
|
+
content: "Content 1",
|
|
190
|
+
correct: true,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
content: "Content 2",
|
|
194
|
+
correct: false,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
content: "Content 3",
|
|
198
|
+
correct: false,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
content: "Content 4",
|
|
202
|
+
correct: false,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
alignment: "default",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
} as PerseusRenderer;
|
|
210
|
+
|
|
211
|
+
export const mockedShuffledRadioProps: {[key in string]: RenderProps} = {
|
|
212
|
+
"radio 1": {
|
|
213
|
+
numCorrect: 1,
|
|
214
|
+
countChoices: false,
|
|
215
|
+
deselectEnabled: false,
|
|
216
|
+
hasNoneOfTheAbove: false,
|
|
217
|
+
multipleSelect: false,
|
|
218
|
+
choices: [
|
|
219
|
+
{
|
|
220
|
+
content: "Content 4",
|
|
221
|
+
correct: false,
|
|
222
|
+
originalIndex: 3,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
content: "Content 2",
|
|
226
|
+
correct: false,
|
|
227
|
+
originalIndex: 1,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
content: "Content 1",
|
|
231
|
+
correct: true,
|
|
232
|
+
originalIndex: 0,
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
content: "Content 3",
|
|
237
|
+
correct: false,
|
|
238
|
+
originalIndex: 2,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
selectedChoices: [false, false, true, false],
|
|
242
|
+
},
|
|
243
|
+
"radio 2": {
|
|
244
|
+
numCorrect: 1,
|
|
245
|
+
countChoices: false,
|
|
246
|
+
deselectEnabled: false,
|
|
247
|
+
hasNoneOfTheAbove: false,
|
|
248
|
+
multipleSelect: false,
|
|
249
|
+
choices: [
|
|
250
|
+
{
|
|
251
|
+
content: "Content 4",
|
|
252
|
+
correct: false,
|
|
253
|
+
originalIndex: 3,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
content: "Content 2",
|
|
257
|
+
correct: false,
|
|
258
|
+
originalIndex: 1,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
content: "Content 1",
|
|
262
|
+
correct: true,
|
|
263
|
+
originalIndex: 0,
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
content: "Content 3",
|
|
268
|
+
correct: false,
|
|
269
|
+
originalIndex: 2,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
selectedChoices: [false, false, true, false],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
question1,
|
|
15
15
|
question2,
|
|
16
16
|
mockedItem,
|
|
17
|
+
mockedRandomItem,
|
|
18
|
+
mockedShuffledRadioProps,
|
|
17
19
|
} from "../__testdata__/renderer.testdata";
|
|
18
20
|
import * as Dependencies from "../dependencies";
|
|
19
21
|
import {Errors} from "../logging/log";
|
|
@@ -29,6 +31,7 @@ import type {
|
|
|
29
31
|
PerseusImageWidgetOptions,
|
|
30
32
|
PerseusInputNumberWidgetOptions,
|
|
31
33
|
} from "../perseus-types";
|
|
34
|
+
import type {APIOptions} from "../types";
|
|
32
35
|
|
|
33
36
|
// NOTE(jeremy): We can't use an automatic mock for the translation linter,
|
|
34
37
|
// because one of it's "instance" methods is created using `debounce` and Jest
|
|
@@ -287,6 +290,20 @@ describe("renderer", () => {
|
|
|
287
290
|
/^less than or equal to$/,
|
|
288
291
|
);
|
|
289
292
|
});
|
|
293
|
+
|
|
294
|
+
it("should call the onWidgetStartProps callback if provided in apiOptions", () => {
|
|
295
|
+
// Arrange
|
|
296
|
+
const onWidgetStartProps = jest.fn();
|
|
297
|
+
const apiOptions: APIOptions = {onWidgetStartProps};
|
|
298
|
+
|
|
299
|
+
// Act
|
|
300
|
+
renderQuestion(mockedRandomItem, apiOptions);
|
|
301
|
+
|
|
302
|
+
// Assert
|
|
303
|
+
expect(onWidgetStartProps).toHaveBeenCalledWith(
|
|
304
|
+
mockedShuffledRadioProps,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
290
307
|
});
|
|
291
308
|
|
|
292
309
|
describe("rendering", () => {
|
|
@@ -157,7 +157,7 @@ class MathInput extends React.Component<Props, State> {
|
|
|
157
157
|
(baseConfig) => ({
|
|
158
158
|
...baseConfig,
|
|
159
159
|
handlers: {
|
|
160
|
-
edit: (mathField) => {
|
|
160
|
+
edit: (mathField: MathFieldInterface) => {
|
|
161
161
|
// This handler is guaranteed to be called on change, but
|
|
162
162
|
// unlike React it sometimes generates false positives.
|
|
163
163
|
// One of these is on initialization (with an empty string
|
|
@@ -184,11 +184,9 @@ class MathInput extends React.Component<Props, State> {
|
|
|
184
184
|
// directly, instead of waiting for `.latex()` to be
|
|
185
185
|
// called in `componentDidUpdate()`.
|
|
186
186
|
const left =
|
|
187
|
-
mathField.
|
|
188
|
-
mathQuillInstance.L
|
|
189
|
-
];
|
|
187
|
+
mathField.cursor()[mathQuillInstance.L];
|
|
190
188
|
if (left && left.ctrlSeq === "\\cdot ") {
|
|
191
|
-
mathField.
|
|
189
|
+
mathField.controller().backspace();
|
|
192
190
|
mathField.cmd("\\times");
|
|
193
191
|
}
|
|
194
192
|
} else {
|
|
@@ -214,7 +212,7 @@ class MathInput extends React.Component<Props, State> {
|
|
|
214
212
|
$(this.__mathFieldWrapperRef).submit();
|
|
215
213
|
}
|
|
216
214
|
},
|
|
217
|
-
upOutOf: (mathField) => {
|
|
215
|
+
upOutOf: (mathField: MathFieldInterface) => {
|
|
218
216
|
// This handler is called when the user presses the up
|
|
219
217
|
// arrow key, but there is nowhere in the expression to go
|
|
220
218
|
// up to (no numerator or exponent). For ease of use,
|
package/src/index.ts
CHANGED
|
@@ -83,7 +83,7 @@ export {
|
|
|
83
83
|
getAnswersFromWidgets,
|
|
84
84
|
injectWidgets,
|
|
85
85
|
} from "./util/extract-perseus-data";
|
|
86
|
-
export {
|
|
86
|
+
export {parsePerseusItem} from "./util/parse-perseus-json";
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Mixins
|
|
@@ -123,11 +123,16 @@ export type {
|
|
|
123
123
|
} from "./types";
|
|
124
124
|
export type {ParsedValue} from "./util";
|
|
125
125
|
export type {
|
|
126
|
-
InputNumberWidget,
|
|
127
|
-
MathFormat,
|
|
128
126
|
PerseusAnswerArea,
|
|
129
127
|
PerseusExpressionWidgetOptions,
|
|
128
|
+
// Utility types
|
|
129
|
+
Range,
|
|
130
|
+
Size,
|
|
131
|
+
MathFormat,
|
|
132
|
+
InputNumberWidget, // TODO(jeremy): remove?
|
|
133
|
+
// Widget configuration types
|
|
130
134
|
PerseusImageBackground,
|
|
135
|
+
PerseusInputNumberWidgetOptions,
|
|
131
136
|
PerseusInteractiveGraphWidgetOptions,
|
|
132
137
|
PerseusItem,
|
|
133
138
|
PerseusPlotterWidgetOptions,
|
package/src/mixins/changeable.ts
CHANGED
package/src/renderer.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
2
2
|
/* eslint-disable react/no-unsafe */
|
|
3
3
|
import * as PerseusLinter from "@khanacademy/perseus-linter";
|
|
4
|
+
import {entries} from "@khanacademy/wonder-stuff-core";
|
|
4
5
|
import classNames from "classnames";
|
|
5
6
|
import $ from "jquery";
|
|
6
7
|
import * as React from "react";
|
|
@@ -41,6 +42,7 @@ import type {
|
|
|
41
42
|
FilterCriterion,
|
|
42
43
|
FocusPath,
|
|
43
44
|
PerseusScore,
|
|
45
|
+
PerseusWidgetMap,
|
|
44
46
|
WidgetProps,
|
|
45
47
|
} from "./types";
|
|
46
48
|
import type {LinterContextProps} from "@khanacademy/perseus-linter";
|
|
@@ -485,18 +487,22 @@ class Renderer extends React.Component<Props, State> {
|
|
|
485
487
|
};
|
|
486
488
|
|
|
487
489
|
_getAllWidgetsStartProps: (
|
|
488
|
-
allWidgetInfo:
|
|
489
|
-
[key: string]: PerseusWidget;
|
|
490
|
-
},
|
|
490
|
+
allWidgetInfo: PerseusWidgetMap,
|
|
491
491
|
props: Props,
|
|
492
|
-
) =>
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
) => PerseusWidgetMap = (allWidgetInfo, props) => {
|
|
493
|
+
const {apiOptions, problemNum} = props;
|
|
494
|
+
const widgetsStartProps: PerseusWidgetMap = {};
|
|
495
|
+
entries(allWidgetInfo).forEach(([key, widgetInfo]) => {
|
|
496
|
+
widgetsStartProps[key] = Widgets.getRendererPropsForWidgetInfo(
|
|
496
497
|
widgetInfo,
|
|
497
|
-
|
|
498
|
+
problemNum,
|
|
498
499
|
);
|
|
499
500
|
});
|
|
501
|
+
|
|
502
|
+
// Call the onWidgetStartProps callback if it exists
|
|
503
|
+
apiOptions?.onWidgetStartProps?.(widgetsStartProps);
|
|
504
|
+
|
|
505
|
+
return widgetsStartProps;
|
|
500
506
|
};
|
|
501
507
|
|
|
502
508
|
// This is only used in _getWidgetInfo as a fallback if widgetId
|
package/src/types.ts
CHANGED
|
@@ -223,6 +223,11 @@ export type APIOptions = Readonly<{
|
|
|
223
223
|
// keystroke caused text typed in the text area to appear in it
|
|
224
224
|
// only after a good few seconds.
|
|
225
225
|
editorChangeDelay?: number;
|
|
226
|
+
// This is a callback function that returns all of the Widget props
|
|
227
|
+
// after they have been transformed by the widget's transform function.
|
|
228
|
+
// This is useful for when we need to know how a widget has shuffled its
|
|
229
|
+
// the available choices.
|
|
230
|
+
onWidgetStartProps?: (widgets: PerseusWidgetMap) => PerseusWidgetMap;
|
|
226
231
|
}>;
|
|
227
232
|
|
|
228
233
|
type TeXProps = {
|
|
@@ -448,6 +453,10 @@ export type WidgetExports<
|
|
|
448
453
|
widget: T;
|
|
449
454
|
}>;
|
|
450
455
|
|
|
456
|
+
export type PerseusWidgetMap = {
|
|
457
|
+
[key: string]: PerseusWidget;
|
|
458
|
+
};
|
|
459
|
+
|
|
451
460
|
export type FilterCriterion =
|
|
452
461
|
| string
|
|
453
462
|
| ((
|