@khanacademy/perseus 19.0.0 → 19.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/es/index.css +209 -104
  3. package/dist/es/index.js +806 -877
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +209 -104
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.js +849 -920
  8. package/dist/index.js.map +1 -1
  9. package/dist/mixins/changeable.d.ts +1 -1
  10. package/dist/perseus-api.d.ts +1 -0
  11. package/dist/renderer.d.ts +2 -4
  12. package/dist/server-item-renderer.d.ts +1 -0
  13. package/dist/types.d.ts +4 -0
  14. package/dist/util/geometry.d.ts +2 -0
  15. package/dist/util/graphie.d.ts +107 -1
  16. package/dist/util/math.d.ts +2 -0
  17. package/dist/util/parse-perseus-json.d.ts +3 -3
  18. package/dist/widgets/expression.d.ts +6 -0
  19. package/dist/widgets/label-image.d.ts +2 -0
  20. package/package.json +3 -4
  21. package/src/__testdata__/renderer.testdata.ts +152 -0
  22. package/src/__tests__/renderer.test.tsx +17 -0
  23. package/src/components/math-input.tsx +4 -6
  24. package/src/index.ts +8 -3
  25. package/src/mixins/changeable.ts +1 -1
  26. package/src/renderer.tsx +14 -8
  27. package/src/types.ts +9 -0
  28. package/src/util/geometry.test.ts +75 -0
  29. package/src/util/geometry.ts +19 -0
  30. package/src/util/graphie.test.ts +1291 -0
  31. package/src/util/graphie.ts +832 -784
  32. package/src/util/interactive.ts +23 -51
  33. package/src/util/math.test.ts +11 -0
  34. package/src/util/math.ts +8 -0
  35. package/src/util/parse-perseus-json.test.ts +3 -3
  36. package/src/util/parse-perseus-json.ts +4 -4
  37. package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +4 -2
  38. package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +4 -2
  39. package/src/widgets/__tests__/expression-mobile.test.tsx +8 -5
  40. package/src/widgets/expression.tsx +1 -23
  41. package/src/widgets/label-image.tsx +0 -1
  42. 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
  };
@@ -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: {
@@ -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;
@@ -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;
@@ -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): 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;
@@ -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 Perseus JSON
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} input - the stringified Perseus JSON
7
+ * @param {string} json - the stringified PerseusItem JSON
8
8
  * @returns {PerseusItem} the parsed PerseusItem object
9
9
  */
10
- export default function parsePerseusJSON(input: string): PerseusItem;
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.0.0",
6
+ "version": "19.1.0",
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.8",
27
- "@khanacademy/math-input": "^16.5.1",
26
+ "@khanacademy/kmath": "^0.1.9",
27
+ "@khanacademy/math-input": "^17.0.0",
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.__controller.cursor[
188
- mathQuillInstance.L
189
- ];
187
+ mathField.cursor()[mathQuillInstance.L];
190
188
  if (left && left.ctrlSeq === "\\cdot ") {
191
- mathField.__controller.backspace();
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 {default as parsePerseusJSON} from "./util/parse-perseus-json";
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,
@@ -127,7 +127,7 @@ export type ChangeableProps = {
127
127
  values: {
128
128
  [key: string]: any;
129
129
  },
130
- callback?: () => unknown | null | undefined,
130
+ callback?: (() => unknown) | null | undefined,
131
131
  silent?: boolean,
132
132
  ) => unknown;
133
133
  };
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
- ) => any = (allWidgetInfo, props) => {
493
- // @ts-expect-error - TS2345 - Argument of type '{ [key: string]: PerseusWidget; }' is not assignable to parameter of type 'Partial<Record<string, CategorizerWidget>>'.
494
- return mapObject(allWidgetInfo, (widgetInfo) => {
495
- return Widgets.getRendererPropsForWidgetInfo(
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
- props.problemNum,
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
  | ((