@khanacademy/perseus 13.2.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/article-renderer.d.ts +1 -2
- package/dist/components/svg-image.d.ts +10 -9
- package/dist/es/index.js +51 -498
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +72 -511
- package/dist/index.js.map +1 -1
- package/dist/multi-items/item-types.d.ts +1 -1
- package/dist/multi-items/multi-renderer.d.ts +2 -2
- package/dist/perseus-api.d.ts +0 -3
- package/dist/server-item-renderer.d.ts +2 -15
- package/dist/strings.js +1 -0
- package/dist/styles/shared.d.ts +1 -0
- package/dist/types.d.ts +0 -2
- package/dist/util/a11y.d.ts +1 -0
- package/dist/widgets/expression.d.ts +0 -12
- package/dist/widgets/graded-group.d.ts +1 -1
- package/dist/widgets/label-image.d.ts +1 -1
- package/dist/widgets/matcher.d.ts +0 -1
- package/dist/widgets/passage.d.ts +1 -1
- package/dist/widgets/radio/widget.d.ts +1 -1
- package/dist/widgets/simulator.d.ts +0 -1
- package/dist/widgets/sorter.d.ts +0 -1
- package/package.json +8 -8
- package/src/__stories__/server-item-renderer.stories.tsx +1 -1
- package/src/__testdata__/server-item-renderer.testdata.ts +160 -0
- package/src/__tests__/article-renderer.test.tsx +2 -3
- package/src/__tests__/server-item-renderer.test.tsx +2 -2
- package/src/article-renderer.tsx +13 -15
- package/src/components/graph.tsx +14 -8
- package/src/components/svg-image.tsx +14 -12
- package/src/hints-renderer.tsx +1 -0
- package/src/index.ts +0 -1
- package/src/multi-items/item-types.ts +1 -1
- package/src/multi-items/multi-renderer.tsx +4 -4
- package/src/perseus-api.tsx +0 -6
- package/src/perseus-types.ts +8 -3
- package/src/server-item-renderer.tsx +12 -13
- package/src/types.ts +0 -5
- package/src/util.ts +1 -1
- package/src/widgets/__stories__/expression.stories.tsx +0 -1
- package/src/widgets/__stories__/random-widgets.stories.tsx +59 -0
- package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -1
- package/src/widgets/__testdata__/categorizer.testdata.ts +40 -0
- package/src/widgets/__testdata__/dropdown.testdata.ts +38 -0
- package/src/widgets/__testdata__/explanation.testdata.ts +30 -0
- package/src/widgets/__testdata__/expression.testdata.ts +82 -0
- package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +269 -0
- package/src/widgets/__testdata__/radio.testdata.ts +63 -0
- package/src/widgets/__testdata__/randomizers.ts +51 -0
- package/src/widgets/__tests__/expression-mobile.test.tsx +0 -2
- package/src/widgets/__tests__/expression.test.tsx +4 -15
- package/src/widgets/__tests__/randomizers.test.ts +53 -0
- package/src/widgets/expression.tsx +1 -5
- package/src/widgets/graded-group.tsx +1 -1
- package/src/widgets/image.tsx +27 -20
- package/src/widgets/label-image/strings.ts +3 -1
- package/src/widgets/label-image.tsx +2 -2
- package/src/widgets/passage-ref.tsx +1 -1
- package/src/widgets/passage.tsx +1 -1
- package/src/widgets/radio/widget.tsx +1 -1
- package/src/widgets/radio.ts +1 -1
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/item-renderer.d.ts +0 -125
- package/dist/mixins/provide-keypad.d.ts +0 -39
- package/src/__stories__/item-renderer.stories.tsx +0 -30
- package/src/__testdata__/item-renderer.testdata.ts +0 -299
- package/src/__tests__/__snapshots__/item-renderer.test.tsx.snap +0 -224
- package/src/__tests__/item-renderer.test.tsx +0 -930
- package/src/item-renderer.tsx +0 -529
- package/src/mixins/provide-keypad.tsx +0 -138
package/src/item-renderer.tsx
DELETED
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
2
|
-
/* eslint-disable react/no-unsafe */
|
|
3
|
-
import * as PerseusLinter from "@khanacademy/perseus-linter";
|
|
4
|
-
import $ from "jquery";
|
|
5
|
-
import * as React from "react";
|
|
6
|
-
import * as ReactDOM from "react-dom";
|
|
7
|
-
import _ from "underscore";
|
|
8
|
-
|
|
9
|
-
import {DependenciesContext} from "./dependencies";
|
|
10
|
-
import HintsRenderer from "./hints-renderer";
|
|
11
|
-
import Objective from "./interactive2/objective_";
|
|
12
|
-
import ProvideKeypad from "./mixins/provide-keypad";
|
|
13
|
-
import {ApiOptions} from "./perseus-api";
|
|
14
|
-
import Renderer from "./renderer";
|
|
15
|
-
import Util from "./util";
|
|
16
|
-
import reactRender from "./util/react-render";
|
|
17
|
-
|
|
18
|
-
import type {KeypadProps} from "./mixins/provide-keypad";
|
|
19
|
-
import type {PerseusItem} from "./perseus-types";
|
|
20
|
-
import type {APIOptions, FocusPath, PerseusDependenciesV2} from "./types";
|
|
21
|
-
import type {KEScore} from "@khanacademy/perseus-core";
|
|
22
|
-
import type {LinterContextProps} from "@khanacademy/perseus-linter";
|
|
23
|
-
|
|
24
|
-
const {mapObject} = Objective;
|
|
25
|
-
|
|
26
|
-
type Props = // These props are used by the ProvideKeypad mixin.
|
|
27
|
-
KeypadProps & {
|
|
28
|
-
// defaults are set in `this.update()` so as to adhere to
|
|
29
|
-
// `ApiOptions.PropTypes`, though the API options that are passed in
|
|
30
|
-
// can be in any degree of completeness
|
|
31
|
-
apiOptions: APIOptions;
|
|
32
|
-
// Whether this component should control hiding/showing peripheral
|
|
33
|
-
// item-related components (for list, see item.answerArea below).
|
|
34
|
-
// TODO(alex): Generalize this to an 'expectsToBeInTemplate' prop
|
|
35
|
-
controlPeripherals?: boolean;
|
|
36
|
-
workAreaSelector: string;
|
|
37
|
-
hintsAreaSelector: string;
|
|
38
|
-
initialHintsVisible?: number;
|
|
39
|
-
item: PerseusItem | any /* any for _multi items */;
|
|
40
|
-
onShowCalculator?: () => unknown;
|
|
41
|
-
onShowChi2Table?: () => unknown;
|
|
42
|
-
onShowPeriodicTable?: (options?: {showKey: boolean}) => unknown;
|
|
43
|
-
onShowTTable?: () => unknown;
|
|
44
|
-
onShowZTable?: () => unknown;
|
|
45
|
-
problemNum: number;
|
|
46
|
-
reviewMode: boolean;
|
|
47
|
-
// TODO(kevinb): make this more precise
|
|
48
|
-
savedState: any;
|
|
49
|
-
linterContext: LinterContextProps;
|
|
50
|
-
legacyPerseusLint?: ReadonlyArray<string>;
|
|
51
|
-
|
|
52
|
-
dependencies: PerseusDependenciesV2;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
type DefaultProps = {
|
|
56
|
-
apiOptions: Props["apiOptions"];
|
|
57
|
-
controlPeripherals: Props["controlPeripherals"];
|
|
58
|
-
hintsAreaSelector: Props["hintsAreaSelector"];
|
|
59
|
-
initialHintsVisible: Props["initialHintsVisible"];
|
|
60
|
-
linterContext: Props["linterContext"];
|
|
61
|
-
reviewMode: Props["reviewMode"];
|
|
62
|
-
workAreaSelector: Props["workAreaSelector"];
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type State = {
|
|
66
|
-
keypadElement: HTMLElement | null | undefined; // from ProvideKeypad.getInitialState,
|
|
67
|
-
hintsVisible: number;
|
|
68
|
-
questionCompleted: boolean;
|
|
69
|
-
questionHighlightedWidgets: ReadonlyArray<string>; // of WidgetIDs
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
type SerializedState = {
|
|
73
|
-
hints: any;
|
|
74
|
-
question: {
|
|
75
|
-
[id: string]: any;
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @depreciated use ServerItemRenderer
|
|
81
|
-
*/
|
|
82
|
-
class ItemRenderer extends React.Component<Props, State> {
|
|
83
|
-
// @ts-expect-error - TS2564 - Property 'questionRenderer' has no initializer and is not definitely assigned in the constructor.
|
|
84
|
-
questionRenderer: Renderer;
|
|
85
|
-
hintsRenderer: React.ElementRef<typeof HintsRenderer> | null | undefined;
|
|
86
|
-
_currentFocus: FocusPath;
|
|
87
|
-
|
|
88
|
-
static defaultProps: DefaultProps = {
|
|
89
|
-
apiOptions: ApiOptions.defaults, // defaults are set in `this.update()`
|
|
90
|
-
controlPeripherals: true,
|
|
91
|
-
hintsAreaSelector: "#hintsarea",
|
|
92
|
-
initialHintsVisible: 0,
|
|
93
|
-
workAreaSelector: "#workarea",
|
|
94
|
-
reviewMode: false,
|
|
95
|
-
linterContext: PerseusLinter.linterContextDefault,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
constructor(props: Props) {
|
|
99
|
-
super(props);
|
|
100
|
-
|
|
101
|
-
this.state = {
|
|
102
|
-
// NOTE: This value is used in provide-keypad.jsx which contains a
|
|
103
|
-
// "mixin" that this file calls methods on.
|
|
104
|
-
...ProvideKeypad.getInitialState.call(this),
|
|
105
|
-
hintsVisible: props.initialHintsVisible || 0,
|
|
106
|
-
questionCompleted: false,
|
|
107
|
-
questionHighlightedWidgets: [],
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
componentDidMount() {
|
|
112
|
-
ProvideKeypad.componentDidMount.call(this);
|
|
113
|
-
if (
|
|
114
|
-
this.props.controlPeripherals &&
|
|
115
|
-
this.props.apiOptions.setDrawingAreaAvailable
|
|
116
|
-
) {
|
|
117
|
-
this.props.apiOptions.setDrawingAreaAvailable(true);
|
|
118
|
-
}
|
|
119
|
-
this._currentFocus = null;
|
|
120
|
-
this.update();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
124
|
-
this.setState({
|
|
125
|
-
questionHighlightedWidgets: [],
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
componentDidUpdate() {
|
|
130
|
-
this.update();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
componentWillUnmount() {
|
|
134
|
-
ProvideKeypad.componentWillUnmount.call(this);
|
|
135
|
-
const workArea = document.querySelector(this.props.workAreaSelector);
|
|
136
|
-
if (workArea != null) {
|
|
137
|
-
ReactDOM.unmountComponentAtNode(workArea);
|
|
138
|
-
}
|
|
139
|
-
const hintsArea = document.querySelector(this.props.hintsAreaSelector);
|
|
140
|
-
if (hintsArea != null) {
|
|
141
|
-
ReactDOM.unmountComponentAtNode(hintsArea);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (this.props.controlPeripherals) {
|
|
145
|
-
const answerArea = this.props.item.answerArea || {};
|
|
146
|
-
if (answerArea.calculator) {
|
|
147
|
-
$("#calculator").hide();
|
|
148
|
-
}
|
|
149
|
-
if (answerArea.periodicTable) {
|
|
150
|
-
$(".periodic-table-info-box").hide();
|
|
151
|
-
}
|
|
152
|
-
if (answerArea.zTable) {
|
|
153
|
-
$(".z-table-info-box").hide();
|
|
154
|
-
}
|
|
155
|
-
if (answerArea.tTable) {
|
|
156
|
-
$(".t-table-info-box").hide();
|
|
157
|
-
}
|
|
158
|
-
if (answerArea.chi2Table) {
|
|
159
|
-
$(".chi2-table-info-box").hide();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
keypadElement(): any {
|
|
165
|
-
return ProvideKeypad.keypadElement.call(this);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
update() {
|
|
169
|
-
const apiOptions = {
|
|
170
|
-
...ApiOptions.defaults,
|
|
171
|
-
...this.props.apiOptions,
|
|
172
|
-
onFocusChange: this._handleFocusChange,
|
|
173
|
-
} as const;
|
|
174
|
-
|
|
175
|
-
const workArea = document.querySelector(this.props.workAreaSelector);
|
|
176
|
-
const hintsArea = document.querySelector(this.props.hintsAreaSelector);
|
|
177
|
-
if (!workArea || !hintsArea) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Since the item renderer works by rendering things into three divs
|
|
182
|
-
// that have completely different places in the DOM, we have to do this
|
|
183
|
-
// strangeness instead of relying on React's normal render() method.
|
|
184
|
-
// TODO(alpert): Figure out how to clean this up somehow
|
|
185
|
-
reactRender(
|
|
186
|
-
<DependenciesContext.Provider value={this.props.dependencies}>
|
|
187
|
-
<Renderer
|
|
188
|
-
ref={(node) => {
|
|
189
|
-
if (!node) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
this.questionRenderer = node;
|
|
193
|
-
|
|
194
|
-
// NOTE(jeremy): Why don't we just pass this into the
|
|
195
|
-
// renderer as a prop?
|
|
196
|
-
const {answerableCallback} = apiOptions;
|
|
197
|
-
if (answerableCallback) {
|
|
198
|
-
const isAnswerable =
|
|
199
|
-
this.questionRenderer.emptyWidgets().length ===
|
|
200
|
-
0;
|
|
201
|
-
answerableCallback(isAnswerable);
|
|
202
|
-
}
|
|
203
|
-
}}
|
|
204
|
-
keypadElement={this.keypadElement()}
|
|
205
|
-
problemNum={this.props.problemNum}
|
|
206
|
-
onInteractWithWidget={this.handleInteractWithWidget}
|
|
207
|
-
highlightedWidgets={this.state.questionHighlightedWidgets}
|
|
208
|
-
apiOptions={apiOptions}
|
|
209
|
-
questionCompleted={this.state.questionCompleted}
|
|
210
|
-
reviewMode={this.props.reviewMode}
|
|
211
|
-
savedState={this.props.savedState}
|
|
212
|
-
linterContext={PerseusLinter.pushContextStack(
|
|
213
|
-
this.props.linterContext,
|
|
214
|
-
"question",
|
|
215
|
-
)}
|
|
216
|
-
{...this.props.item.question}
|
|
217
|
-
legacyPerseusLint={this.props.legacyPerseusLint}
|
|
218
|
-
/>
|
|
219
|
-
</DependenciesContext.Provider>,
|
|
220
|
-
// @ts-expect-error - TS2345 - Argument of type 'Element' is not assignable to parameter of type 'HTMLElement'.
|
|
221
|
-
workArea,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
reactRender(
|
|
225
|
-
<DependenciesContext.Provider value={this.props.dependencies}>
|
|
226
|
-
<HintsRenderer
|
|
227
|
-
ref={(node) => (this.hintsRenderer = node)}
|
|
228
|
-
hints={this.props.item.hints}
|
|
229
|
-
hintsVisible={this.state.hintsVisible}
|
|
230
|
-
apiOptions={apiOptions}
|
|
231
|
-
linterContext={PerseusLinter.pushContextStack(
|
|
232
|
-
this.props.linterContext,
|
|
233
|
-
"hints",
|
|
234
|
-
)}
|
|
235
|
-
/>
|
|
236
|
-
</DependenciesContext.Provider>,
|
|
237
|
-
|
|
238
|
-
// @ts-expect-error - TS2345 - Argument of type 'Element' is not assignable to parameter of type 'HTMLElement'.
|
|
239
|
-
hintsArea,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const answerArea = this.props.item.answerArea || {};
|
|
243
|
-
if (this.props.controlPeripherals) {
|
|
244
|
-
$("#calculator").toggle(answerArea.calculator || false);
|
|
245
|
-
$(".periodic-table-info-box").toggle(
|
|
246
|
-
answerArea.periodicTable || false,
|
|
247
|
-
);
|
|
248
|
-
$(".z-table-info-box").toggle(answerArea.zTable || false);
|
|
249
|
-
$(".t-table-info-box").toggle(answerArea.tTable || false);
|
|
250
|
-
$(".chi2-table-info-box").toggle(answerArea.chi2Table || false);
|
|
251
|
-
} else {
|
|
252
|
-
if (answerArea.calculator) {
|
|
253
|
-
this.props.onShowCalculator && this.props.onShowCalculator();
|
|
254
|
-
}
|
|
255
|
-
if (answerArea.periodicTable) {
|
|
256
|
-
this.props.onShowPeriodicTable &&
|
|
257
|
-
this.props.onShowPeriodicTable({
|
|
258
|
-
showKey: answerArea.periodicTableWithKey,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
if (answerArea.zTable) {
|
|
262
|
-
this.props.onShowZTable && this.props.onShowZTable();
|
|
263
|
-
}
|
|
264
|
-
if (answerArea.tTable) {
|
|
265
|
-
this.props.onShowTTable && this.props.onShowTTable();
|
|
266
|
-
}
|
|
267
|
-
if (answerArea.chi2Table) {
|
|
268
|
-
this.props.onShowChi2Table && this.props.onShowChi2Table();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
_handleFocusChange: (newFocus: FocusPath, oldFocus: FocusPath) => void = (
|
|
274
|
-
newFocus: FocusPath,
|
|
275
|
-
oldFocus: FocusPath,
|
|
276
|
-
) => {
|
|
277
|
-
if (newFocus != null) {
|
|
278
|
-
this._setCurrentFocus(newFocus);
|
|
279
|
-
} else {
|
|
280
|
-
this._onRendererBlur(oldFocus);
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// Sets the current focus path and element and send an onChangeFocus event
|
|
285
|
-
// back to our parent.
|
|
286
|
-
_setCurrentFocus(newFocus: FocusPath) {
|
|
287
|
-
const keypadElement = this.keypadElement();
|
|
288
|
-
// By the time this happens, newFocus cannot be a prefix of
|
|
289
|
-
// prevFocused, since we must have either been called from
|
|
290
|
-
// an onFocusChange within a renderer, which is only called when
|
|
291
|
-
// this is not a prefix, or between the question and answer areas,
|
|
292
|
-
// which can never prefix each other.
|
|
293
|
-
const prevFocus = this._currentFocus;
|
|
294
|
-
this._currentFocus = newFocus;
|
|
295
|
-
|
|
296
|
-
// Determine whether the newly focused path represents an input.
|
|
297
|
-
const inputPaths = this.getInputPaths();
|
|
298
|
-
const didFocusInput =
|
|
299
|
-
this._currentFocus &&
|
|
300
|
-
inputPaths.some((inputPath) => {
|
|
301
|
-
return Util.inputPathsEqual(inputPath, this._currentFocus);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
if (this.props.apiOptions.onFocusChange != null) {
|
|
305
|
-
this.props.apiOptions.onFocusChange(
|
|
306
|
-
this._currentFocus,
|
|
307
|
-
prevFocus,
|
|
308
|
-
didFocusInput && keypadElement && keypadElement.getDOMNode(),
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (keypadElement) {
|
|
313
|
-
if (didFocusInput) {
|
|
314
|
-
keypadElement.activate();
|
|
315
|
-
} else {
|
|
316
|
-
keypadElement.dismiss();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
_onRendererBlur(blurPath: FocusPath) {
|
|
322
|
-
const blurringFocusPath = this._currentFocus;
|
|
323
|
-
|
|
324
|
-
// Failsafe: abort if ID is different, because focus probably happened
|
|
325
|
-
// before blur.
|
|
326
|
-
if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Wait until after any new focus events fire this tick before
|
|
331
|
-
// declaring that nothing is focused, since if there were a focus change
|
|
332
|
-
// across Renderers (e.g., from the HintsRenderer to the
|
|
333
|
-
// QuestionRenderer), we could receive the blur before the focus.
|
|
334
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
335
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
336
|
-
setTimeout(() => {
|
|
337
|
-
if (Util.inputPathsEqual(this._currentFocus, blurringFocusPath)) {
|
|
338
|
-
this._setCurrentFocus(null);
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Accepts a question area widgetId, or an answer area widgetId of
|
|
345
|
-
* the form "answer-input-number 1", or the string "answer-area"
|
|
346
|
-
* for the whole answer area (if the answer area is a single widget).
|
|
347
|
-
*/
|
|
348
|
-
_setWidgetProps(widgetId: string, newProps: any, callback: any) {
|
|
349
|
-
this.questionRenderer._setWidgetProps(widgetId, newProps, callback);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Sets the input value into the widget found at `path`. The `focus`
|
|
354
|
-
* function provided is called when all affected widgets have been
|
|
355
|
-
* re-rendered with the new value.
|
|
356
|
-
*/
|
|
357
|
-
setInputValue(path: FocusPath, newValue: string, focus: () => void): void {
|
|
358
|
-
return this.questionRenderer.setInputValue(path, newValue, focus);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
focusPath(path: FocusPath): void {
|
|
362
|
-
return this.questionRenderer.focusPath(path);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
blurPath(path: FocusPath): void {
|
|
366
|
-
return this.questionRenderer.blurPath(path);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
getDOMNodeForPath(path: FocusPath): Element | Text | null | undefined {
|
|
370
|
-
return this.questionRenderer.getDOMNodeForPath(path);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
getGrammarTypeForPath(path: FocusPath): string | null | undefined {
|
|
374
|
-
return this.questionRenderer.getGrammarTypeForPath(path);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
getInputPaths(): ReadonlyArray<FocusPath> {
|
|
378
|
-
const questionAreaInputPaths = this.questionRenderer.getInputPaths();
|
|
379
|
-
return questionAreaInputPaths;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// NOTE: this must remain as an arrow function because we pass it Renderer
|
|
383
|
-
handleInteractWithWidget: (widgetId: string) => void = (
|
|
384
|
-
widgetId: string,
|
|
385
|
-
) => {
|
|
386
|
-
const withRemoved = _.difference(
|
|
387
|
-
this.state.questionHighlightedWidgets,
|
|
388
|
-
[widgetId],
|
|
389
|
-
);
|
|
390
|
-
this.setState({
|
|
391
|
-
questionCompleted: false,
|
|
392
|
-
questionHighlightedWidgets: withRemoved,
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
if (this.props.apiOptions.interactionCallback) {
|
|
396
|
-
this.props.apiOptions.interactionCallback();
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
focus(): boolean | null | undefined {
|
|
401
|
-
return this.questionRenderer.focus();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
blur() {
|
|
405
|
-
if (this._currentFocus) {
|
|
406
|
-
this.blurPath(this._currentFocus);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
showHint() {
|
|
411
|
-
if (this.state.hintsVisible < this.getNumHints()) {
|
|
412
|
-
this.setState({
|
|
413
|
-
hintsVisible: this.state.hintsVisible + 1,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
getNumHints(): number {
|
|
419
|
-
return this.props.item.hints.length;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Grades the item.
|
|
424
|
-
*
|
|
425
|
-
* Returns a KE-style score of {
|
|
426
|
-
* empty: bool,
|
|
427
|
-
* correct: bool,
|
|
428
|
-
* message: string|null,
|
|
429
|
-
* guess: Array
|
|
430
|
-
* }
|
|
431
|
-
*/
|
|
432
|
-
scoreInput(): KEScore {
|
|
433
|
-
const guessAndScore = this.questionRenderer.guessAndScore();
|
|
434
|
-
const guess = guessAndScore[0];
|
|
435
|
-
const score = guessAndScore[1];
|
|
436
|
-
|
|
437
|
-
// Continue to include an empty guess for the now defunct answer area.
|
|
438
|
-
// TODO(alex): Check whether we rely on the format here for
|
|
439
|
-
// analyzing ProblemLogs. If not, remove this layer.
|
|
440
|
-
const maxCompatGuess = [guess, []];
|
|
441
|
-
|
|
442
|
-
const keScore = Util.keScoreFromPerseusScore(
|
|
443
|
-
score,
|
|
444
|
-
maxCompatGuess,
|
|
445
|
-
this.questionRenderer.getSerializedState(),
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
449
|
-
|
|
450
|
-
this.setState({
|
|
451
|
-
questionCompleted: keScore.correct,
|
|
452
|
-
questionHighlightedWidgets: emptyQuestionAreaWidgets,
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
return keScore;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Returns an array of all widget IDs in the order they occur in
|
|
460
|
-
* the question content.
|
|
461
|
-
*/
|
|
462
|
-
getWidgetIds(): ReadonlyArray<string> {
|
|
463
|
-
return this.questionRenderer.getWidgetIds();
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Returns an object mapping from widget ID to KE-style score.
|
|
468
|
-
* The keys of this object are the values of the array returned
|
|
469
|
-
* from `getWidgetIds`.
|
|
470
|
-
*/
|
|
471
|
-
scoreWidgets(): {
|
|
472
|
-
[id: string]: KEScore;
|
|
473
|
-
} {
|
|
474
|
-
const qScore = this.questionRenderer.scoreWidgets();
|
|
475
|
-
const qGuess = this.questionRenderer.getUserInputForWidgets();
|
|
476
|
-
const state = this.questionRenderer.getSerializedState();
|
|
477
|
-
// @ts-expect-error - TS2322 - Type 'Partial<Record<string, KEScore>>' is not assignable to type '{ [id: string]: KEScore; }'. | TS2345 - Argument of type '{ [widgetId: string]: PerseusScore; }' is not assignable to parameter of type 'Partial<Record<string, { type: "invalid"; message?: string | null | undefined; suppressAlmostThere?: boolean | null | undefined; }>>'.
|
|
478
|
-
return mapObject(qScore, (score, id) => {
|
|
479
|
-
return Util.keScoreFromPerseusScore(score, qGuess[id], state[id]);
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Get a representation of the current state of the item.
|
|
485
|
-
*/
|
|
486
|
-
getSerializedState(): SerializedState {
|
|
487
|
-
return {
|
|
488
|
-
question: this.questionRenderer.getSerializedState(),
|
|
489
|
-
hints: this.hintsRenderer?.getSerializedState(),
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// TODO(kevinb): make the type for this state better
|
|
494
|
-
restoreSerializedState(state: SerializedState, callback?: () => void) {
|
|
495
|
-
// We need to wait for both the question renderer and the hints
|
|
496
|
-
// renderer to finish restoring their states.
|
|
497
|
-
let numCallbacks = 2;
|
|
498
|
-
const fireCallback = () => {
|
|
499
|
-
--numCallbacks;
|
|
500
|
-
if (callback && numCallbacks === 0) {
|
|
501
|
-
callback();
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
this.questionRenderer.restoreSerializedState(
|
|
506
|
-
state.question,
|
|
507
|
-
fireCallback,
|
|
508
|
-
);
|
|
509
|
-
this.hintsRenderer?.restoreSerializedState(state.hints, fireCallback);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
showRationalesForCurrentlySelectedChoices() {
|
|
513
|
-
this.questionRenderer.showRationalesForCurrentlySelectedChoices();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
deselectIncorrectSelectedChoices() {
|
|
517
|
-
this.questionRenderer.deselectIncorrectSelectedChoices();
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
render(): React.ReactNode {
|
|
521
|
-
return (
|
|
522
|
-
<DependenciesContext.Provider value={this.props.dependencies}>
|
|
523
|
-
<div />
|
|
524
|
-
</DependenciesContext.Provider>
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
export default ItemRenderer;
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
2
|
-
/**
|
|
3
|
-
* A mixin that renders a custom software keypad in additional to the base
|
|
4
|
-
* component. The base component will receive blur events when the keypad is
|
|
5
|
-
* dismissed and can access the keypad element itself so as to manage its
|
|
6
|
-
* activation and dismissal.
|
|
7
|
-
*
|
|
8
|
-
* TODO(charlie): This would make a nicer higher-order component, except that
|
|
9
|
-
* we need to expose methods on the base component (i.e., `ItemRenderer`). When
|
|
10
|
-
* `ItemRenderer` and friends are written as ES6 Classes, we can have them
|
|
11
|
-
* extend a `ProvideKeypad` component instead of using this mixin.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {MobileKeypad} from "@khanacademy/math-input";
|
|
15
|
-
import * as React from "react";
|
|
16
|
-
import ReactDOM from "react-dom";
|
|
17
|
-
|
|
18
|
-
import reactRender from "../util/react-render";
|
|
19
|
-
|
|
20
|
-
import type {APIOptions} from "../types";
|
|
21
|
-
import type {KeypadAPI} from "@khanacademy/math-input";
|
|
22
|
-
import type {CSSProperties} from "aphrodite";
|
|
23
|
-
|
|
24
|
-
export type KeypadProps = {
|
|
25
|
-
// An Aphrodite style object, to be applied to the keypad container.
|
|
26
|
-
// Note that, given our awkward structure of injecting the keypad, this
|
|
27
|
-
// style won't be applied or updated dynamically. Rather, it will only
|
|
28
|
-
// be applied in `componentDidMount`.
|
|
29
|
-
keypadStyle?: CSSProperties;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type KeypadApiOptions = {
|
|
33
|
-
customKeypad: boolean;
|
|
34
|
-
nativeKeypadProxy: any;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// This represents the shape of any component that uses this ProvideKeypad
|
|
38
|
-
// mixin.
|
|
39
|
-
type KeypadMixinHostComponent = {
|
|
40
|
-
_keypadContainer?: HTMLDivElement | null;
|
|
41
|
-
props: {
|
|
42
|
-
apiOptions: APIOptions;
|
|
43
|
-
blur?: () => void;
|
|
44
|
-
};
|
|
45
|
-
state: {
|
|
46
|
-
keypadElement: any | null;
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// NOTE: This is not a real component. It's a collection of methods used to
|
|
51
|
-
// create and manage a Keypad instances.
|
|
52
|
-
// TODO(LP-10789): replace this with a React Context object to pass information
|
|
53
|
-
// between Perseus and the Keypad.
|
|
54
|
-
const ProvideKeypad = {
|
|
55
|
-
getInitialState(this: KeypadMixinHostComponent): {
|
|
56
|
-
keypadElement: any | null;
|
|
57
|
-
} {
|
|
58
|
-
const _this = this;
|
|
59
|
-
let keypadElement: KeypadAPI | null = null;
|
|
60
|
-
if (
|
|
61
|
-
_this.props.apiOptions &&
|
|
62
|
-
_this.props.apiOptions.customKeypad &&
|
|
63
|
-
_this.props.apiOptions.nativeKeypadProxy
|
|
64
|
-
) {
|
|
65
|
-
keypadElement = _this.props.apiOptions.nativeKeypadProxy(
|
|
66
|
-
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'. | TS2339 - Property 'blur' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'.
|
|
67
|
-
() => _this.blur && _this.blur(),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
return {keypadElement};
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
componentDidMount(this: KeypadMixinHostComponent) {
|
|
74
|
-
const _this = this;
|
|
75
|
-
const apiOptions: APIOptions = _this.props.apiOptions;
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
apiOptions &&
|
|
79
|
-
apiOptions.customKeypad &&
|
|
80
|
-
!apiOptions.nativeKeypadProxy
|
|
81
|
-
) {
|
|
82
|
-
// TODO(charlie): Render this and the wrapped component in the same
|
|
83
|
-
// React tree. We may also want to add this keypad asynchronously
|
|
84
|
-
// or on-demand in the future.
|
|
85
|
-
_this._keypadContainer = document.createElement("div");
|
|
86
|
-
document.body?.appendChild(_this._keypadContainer);
|
|
87
|
-
|
|
88
|
-
reactRender(
|
|
89
|
-
<MobileKeypad
|
|
90
|
-
onElementMounted={(element) => {
|
|
91
|
-
// NOTE(kevinb): The reason why this setState works is
|
|
92
|
-
// b/c we're calling it manually from item-renderer.jsx
|
|
93
|
-
// and we're manually setting the 'this' by using 'call'
|
|
94
|
-
// @ts-expect-error - TS2339 - Property 'setState' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'.
|
|
95
|
-
_this.setState({
|
|
96
|
-
keypadElement: element,
|
|
97
|
-
});
|
|
98
|
-
}}
|
|
99
|
-
onDismiss={() => {
|
|
100
|
-
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'. | TS2339 - Property 'blur' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'.
|
|
101
|
-
_this.blur && _this.blur();
|
|
102
|
-
}}
|
|
103
|
-
// @ts-expect-error - TS2339 - Property 'props' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'.
|
|
104
|
-
style={_this.props.keypadStyle}
|
|
105
|
-
useV2Keypad={apiOptions.useV2Keypad}
|
|
106
|
-
onAnalyticsEvent={async () => {
|
|
107
|
-
// Intentionally left empty. This component is
|
|
108
|
-
// deprecated and so we don't want/need to instrument
|
|
109
|
-
// it.
|
|
110
|
-
}}
|
|
111
|
-
/>,
|
|
112
|
-
_this._keypadContainer,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
componentWillUnmount(this: KeypadMixinHostComponent) {
|
|
118
|
-
const _this = this;
|
|
119
|
-
if (_this._keypadContainer) {
|
|
120
|
-
ReactDOM.unmountComponentAtNode(_this._keypadContainer);
|
|
121
|
-
if (_this._keypadContainer.parentNode) {
|
|
122
|
-
// Note ChildNode.remove() isn't available in older Android
|
|
123
|
-
// webviews.
|
|
124
|
-
_this._keypadContainer.parentNode.removeChild(
|
|
125
|
-
_this._keypadContainer,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
_this._keypadContainer = null;
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
keypadElement(this: KeypadMixinHostComponent): any {
|
|
133
|
-
const _this = this;
|
|
134
|
-
return _this.state.keypadElement;
|
|
135
|
-
},
|
|
136
|
-
} as const;
|
|
137
|
-
|
|
138
|
-
export default ProvideKeypad;
|