@khanacademy/perseus-editor 2.6.8 → 2.7.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 +20 -0
- package/dist/es/index.css +3 -0
- package/dist/es/index.js +18 -49
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +3 -0
- package/dist/index.js +29 -65
- package/dist/index.js.map +1 -1
- package/dist/widgets/expression-editor.d.ts +19 -17
- package/package.json +4 -2
- package/src/styles/perseus-editor.less +5 -0
- package/src/widgets/__stories__/expression-editor.stories.tsx +1 -2
- package/src/widgets/expression-editor.tsx +78 -79
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Changeable } from "@khanacademy/perseus";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
import type { PerseusExpressionWidgetOptions } from "@khanacademy/perseus";
|
|
4
|
+
type Props = {
|
|
5
|
+
widgetId?: any;
|
|
6
|
+
value?: string;
|
|
7
|
+
} & Omit<PerseusExpressionWidgetOptions, "buttonsVisible"> & Changeable.ChangeableProps;
|
|
8
|
+
type DefaultProps = {
|
|
9
|
+
answerForms: Props["answerForms"];
|
|
10
|
+
times: Props["times"];
|
|
11
|
+
buttonSets: Props["buttonSets"];
|
|
12
|
+
functions: Props["functions"];
|
|
13
|
+
};
|
|
14
|
+
type State = {
|
|
15
|
+
isTex: boolean;
|
|
16
|
+
};
|
|
17
|
+
declare class ExpressionEditor extends React.Component<Props, State> {
|
|
16
18
|
static widgetName: "expression";
|
|
17
|
-
static defaultProps:
|
|
18
|
-
constructor(props:
|
|
19
|
-
change
|
|
19
|
+
static defaultProps: DefaultProps;
|
|
20
|
+
constructor(props: Props);
|
|
21
|
+
change(...args: any[]): any;
|
|
20
22
|
render(): React.ReactNode;
|
|
21
23
|
serialize: () => any;
|
|
22
24
|
getSaveWarnings: () => any;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Perseus editors",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.7.1",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@khanacademy/kas": "^0.3.1",
|
|
26
26
|
"@khanacademy/kmath": "^0.1.2",
|
|
27
|
-
"@khanacademy/perseus": "^11.1
|
|
27
|
+
"@khanacademy/perseus": "^11.2.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@khanacademy/wonder-blocks-button": "^4.1.3",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@khanacademy/wonder-blocks-i18n": "^2.0.2",
|
|
35
35
|
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
|
|
36
36
|
"@khanacademy/wonder-blocks-typography": "^2.1.3",
|
|
37
|
+
"@khanacademy/wonder-stuff-core": "^1.5.1",
|
|
37
38
|
"aphrodite": "^1.2.5",
|
|
38
39
|
"classnames": "^1.1.4",
|
|
39
40
|
"create-react-class": "^15.6.3",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"@khanacademy/wonder-blocks-i18n": "^2.0.2",
|
|
54
55
|
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
|
|
55
56
|
"@khanacademy/wonder-blocks-typography": "^2.1.3",
|
|
57
|
+
"@khanacademy/wonder-stuff-core": "^1.5.1",
|
|
56
58
|
"aphrodite": "^1.2.5",
|
|
57
59
|
"classnames": "^1.1.4",
|
|
58
60
|
"create-react-class": "^15.6.3",
|
|
@@ -62,8 +62,7 @@ class WithDebug extends React.Component<Empty, State> {
|
|
|
62
62
|
<div className={css(styles.editorWrapper)}>
|
|
63
63
|
<ExpressionEditor
|
|
64
64
|
{...this.state}
|
|
65
|
-
|
|
66
|
-
onChange={(props) => {
|
|
65
|
+
onChange={(props: PerseusExpressionWidgetOptions) => {
|
|
67
66
|
this.setState({
|
|
68
67
|
...props,
|
|
69
68
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint-disable
|
|
1
|
+
/* eslint-disable react/sort-comp */
|
|
2
2
|
import * as KAS from "@khanacademy/kas";
|
|
3
3
|
import {
|
|
4
4
|
components,
|
|
@@ -7,24 +7,31 @@ import {
|
|
|
7
7
|
Expression,
|
|
8
8
|
PerseusExpressionAnswerFormConsidered,
|
|
9
9
|
} from "@khanacademy/perseus";
|
|
10
|
+
import {isTruthy} from "@khanacademy/wonder-stuff-core";
|
|
10
11
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
11
12
|
import lens from "hubble";
|
|
12
|
-
import PropTypes from "prop-types";
|
|
13
13
|
import * as React from "react";
|
|
14
14
|
import _ from "underscore";
|
|
15
15
|
|
|
16
16
|
import SortableArea from "../components/sortable";
|
|
17
17
|
|
|
18
|
+
import type {PerseusExpressionWidgetOptions} from "@khanacademy/perseus";
|
|
19
|
+
|
|
18
20
|
const {InfoTip, PropCheckBox, TexButtons} = components;
|
|
19
21
|
const {getDependencies} = Dependencies;
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
type Props = {
|
|
24
|
+
widgetId?: any;
|
|
25
|
+
value?: string;
|
|
26
|
+
} & Omit<PerseusExpressionWidgetOptions, "buttonsVisible"> &
|
|
27
|
+
Changeable.ChangeableProps;
|
|
28
|
+
|
|
29
|
+
type DefaultProps = {
|
|
30
|
+
answerForms: Props["answerForms"];
|
|
31
|
+
times: Props["times"];
|
|
32
|
+
buttonSets: Props["buttonSets"];
|
|
33
|
+
functions: Props["functions"];
|
|
34
|
+
};
|
|
28
35
|
|
|
29
36
|
// Pick a key that isn't currently used by an answer in answerForms
|
|
30
37
|
const _makeNewKey = (answerForms: any) => {
|
|
@@ -48,25 +55,21 @@ const _makeNewKey = (answerForms: any) => {
|
|
|
48
55
|
return usedKeys.length;
|
|
49
56
|
};
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
answerForms: PropTypes.arrayOf(answerFormType),
|
|
55
|
-
times: PropTypes.bool,
|
|
56
|
-
buttonSets: TexButtons.buttonSetsType,
|
|
57
|
-
functions: PropTypes.arrayOf(PropTypes.string),
|
|
58
|
-
};
|
|
58
|
+
type State = {
|
|
59
|
+
isTex: boolean;
|
|
60
|
+
};
|
|
59
61
|
|
|
62
|
+
class ExpressionEditor extends React.Component<Props, State> {
|
|
60
63
|
static widgetName = "expression" as const;
|
|
61
64
|
|
|
62
|
-
static defaultProps:
|
|
65
|
+
static defaultProps: DefaultProps = {
|
|
63
66
|
answerForms: [],
|
|
64
67
|
times: false,
|
|
65
68
|
buttonSets: ["basic"],
|
|
66
69
|
functions: ["f", "g", "h"],
|
|
67
70
|
};
|
|
68
71
|
|
|
69
|
-
constructor(props:
|
|
72
|
+
constructor(props: Props) {
|
|
70
73
|
super(props);
|
|
71
74
|
// Is the format of `value` TeX or plain text?
|
|
72
75
|
// TODO(alex): Remove after backfilling everything to TeX
|
|
@@ -79,22 +82,19 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
79
82
|
if (props.answerForms.length === 0) {
|
|
80
83
|
isTex = true;
|
|
81
84
|
} else {
|
|
82
|
-
isTex =
|
|
85
|
+
isTex = props.answerForms.some((form) => {
|
|
83
86
|
const {value} = form;
|
|
84
87
|
// only TeX has backslashes and curly braces
|
|
85
|
-
return (
|
|
86
|
-
_.indexOf(value, "\\") !== -1 ||
|
|
87
|
-
_.indexOf(value, "{") !== -1
|
|
88
|
-
);
|
|
88
|
+
return value.indexOf("\\") !== -1 || value.indexOf("{") !== -1;
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
this.state = {isTex};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
change
|
|
95
|
+
change(...args) {
|
|
96
96
|
return Changeable.change.apply(this, args);
|
|
97
|
-
}
|
|
97
|
+
}
|
|
98
98
|
|
|
99
99
|
render(): React.ReactNode {
|
|
100
100
|
const answerOptions = this.props.answerForms
|
|
@@ -138,26 +138,28 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
138
138
|
);
|
|
139
139
|
|
|
140
140
|
// checkboxes to choose which sets of input buttons are shown
|
|
141
|
-
const buttonSetChoices =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
141
|
+
const buttonSetChoices = Object.keys(TexButtons.buttonSets).map(
|
|
142
|
+
(name) => {
|
|
143
|
+
// The first one gets special cased to always be checked, disabled,
|
|
144
|
+
// and float left.
|
|
145
|
+
const isFirst = name === "basic";
|
|
146
|
+
const checked = this.props.buttonSets.includes(name) || isFirst;
|
|
147
|
+
const className = isFirst
|
|
148
|
+
? "button-set-label-float"
|
|
149
|
+
: "button-set-label";
|
|
150
|
+
return (
|
|
151
|
+
<label className={className} key={name}>
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
checked={checked}
|
|
155
|
+
disabled={isFirst}
|
|
156
|
+
onChange={() => this.handleButtonSet(name)}
|
|
157
|
+
/>
|
|
158
|
+
{name}
|
|
159
|
+
</label>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
161
163
|
|
|
162
164
|
const {TeX} = getDependencies(); // OldExpression only
|
|
163
165
|
|
|
@@ -282,7 +284,7 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
282
284
|
if (this.props.answerForms.length === 0) {
|
|
283
285
|
issues.push("No answers specified");
|
|
284
286
|
} else {
|
|
285
|
-
const hasCorrect =
|
|
287
|
+
const hasCorrect = this.props.answerForms.some((form) => {
|
|
286
288
|
return form.considered === "correct";
|
|
287
289
|
});
|
|
288
290
|
if (!hasCorrect) {
|
|
@@ -337,14 +339,12 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
337
339
|
newAnswer: () => void = () => {
|
|
338
340
|
const answerForms = this.props.answerForms.slice();
|
|
339
341
|
answerForms.push(this._newEmptyAnswerForm());
|
|
340
|
-
// @ts-expect-error - TS2554 - Expected 3 arguments, but got 1.
|
|
341
342
|
this.change({answerForms});
|
|
342
343
|
};
|
|
343
344
|
|
|
344
345
|
handleRemoveForm: (arg1: number) => void = (i) => {
|
|
345
346
|
const answerForms = this.props.answerForms.slice();
|
|
346
347
|
answerForms.splice(i, 1);
|
|
347
|
-
// @ts-expect-error - TS2554 - Expected 3 arguments, but got 1.
|
|
348
348
|
this.change({answerForms});
|
|
349
349
|
};
|
|
350
350
|
|
|
@@ -355,12 +355,11 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
355
355
|
.merge([i], props)
|
|
356
356
|
.freeze();
|
|
357
357
|
|
|
358
|
-
// @ts-expect-error - TS2554 - Expected 3 arguments, but got 1.
|
|
359
358
|
this.change({answerForms});
|
|
360
359
|
};
|
|
361
360
|
|
|
362
361
|
handleReorder: (arg1: any) => void = (components) => {
|
|
363
|
-
const answerForms =
|
|
362
|
+
const answerForms = components.map((component) => {
|
|
364
363
|
const form = _(component.props).pick(
|
|
365
364
|
"considered",
|
|
366
365
|
"form",
|
|
@@ -372,20 +371,18 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
372
371
|
return form;
|
|
373
372
|
});
|
|
374
373
|
|
|
375
|
-
// @ts-expect-error - TS2554 - Expected 3 arguments, but got 1.
|
|
376
374
|
this.change({answerForms});
|
|
377
375
|
};
|
|
378
376
|
|
|
379
377
|
// called when the selected buttonset changes
|
|
380
378
|
handleButtonSet: (arg1: string) => void = (changingName) => {
|
|
381
|
-
const buttonSetNames =
|
|
379
|
+
const buttonSetNames = Object.keys(TexButtons.buttonSets);
|
|
382
380
|
|
|
383
381
|
// Filter to preserve order - using .union and .difference would always
|
|
384
382
|
// move the last added button set to the end.
|
|
385
|
-
const buttonSets =
|
|
383
|
+
const buttonSets = buttonSetNames.filter((set) => {
|
|
386
384
|
return (
|
|
387
|
-
|
|
388
|
-
(set === changingName)
|
|
385
|
+
this.props.buttonSets.includes(set) !== (set === changingName)
|
|
389
386
|
);
|
|
390
387
|
});
|
|
391
388
|
|
|
@@ -397,8 +394,9 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
397
394
|
// "basic+div". Toggle between the two of them.
|
|
398
395
|
// If someone can think of a more elegant formulation of this (there
|
|
399
396
|
// must be one!) feel free to change it.
|
|
400
|
-
let keep
|
|
401
|
-
|
|
397
|
+
let keep: string | undefined;
|
|
398
|
+
let remove: string | undefined;
|
|
399
|
+
if (this.props.buttonSets.includes("basic+div")) {
|
|
402
400
|
keep = "basic";
|
|
403
401
|
remove = "basic+div";
|
|
404
402
|
} else {
|
|
@@ -406,11 +404,10 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
406
404
|
remove = "basic";
|
|
407
405
|
}
|
|
408
406
|
|
|
409
|
-
const buttonSets =
|
|
410
|
-
.
|
|
407
|
+
const buttonSets = this.props.buttonSets
|
|
408
|
+
.filter((set) => set !== remove)
|
|
411
409
|
.concat(keep);
|
|
412
410
|
|
|
413
|
-
// @ts-expect-error - TS2554 - Expected 3 arguments, but got 2.
|
|
414
411
|
this.change("buttonSets", buttonSets);
|
|
415
412
|
};
|
|
416
413
|
|
|
@@ -426,7 +423,7 @@ class ExpressionEditor extends React.Component<any, any> {
|
|
|
426
423
|
e,
|
|
427
424
|
) => {
|
|
428
425
|
const newProps: Record<string, any> = {};
|
|
429
|
-
newProps.functions =
|
|
426
|
+
newProps.functions = e.target.value.split(/[ ,]+/).filter(isTruthy);
|
|
430
427
|
this.props.onChange(newProps);
|
|
431
428
|
};
|
|
432
429
|
}
|
|
@@ -438,24 +435,28 @@ const findNextIn = function (arr: ReadonlyArray<string>, val: any) {
|
|
|
438
435
|
return arr[ix];
|
|
439
436
|
};
|
|
440
437
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
considered: PropTypes.oneOf(PerseusExpressionAnswerFormConsidered)
|
|
445
|
-
.isRequired,
|
|
446
|
-
expressionProps: PropTypes.object.isRequired,
|
|
438
|
+
type AnswerOptionProps = {
|
|
439
|
+
considered: typeof PerseusExpressionAnswerFormConsidered[number];
|
|
440
|
+
expressionProps: any;
|
|
447
441
|
|
|
448
|
-
|
|
449
|
-
|
|
442
|
+
// Must the answer have the same form as this answer.
|
|
443
|
+
form: boolean;
|
|
450
444
|
|
|
451
|
-
|
|
452
|
-
|
|
445
|
+
// Must the answer be simplified.
|
|
446
|
+
simplify: boolean;
|
|
453
447
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
448
|
+
onDelete: () => void;
|
|
449
|
+
} & Changeable.ChangeableProps;
|
|
450
|
+
|
|
451
|
+
type AnswerOptionState = {
|
|
452
|
+
deleteFocused: boolean;
|
|
453
|
+
};
|
|
457
454
|
|
|
458
|
-
|
|
455
|
+
class AnswerOption extends React.Component<
|
|
456
|
+
AnswerOptionProps,
|
|
457
|
+
AnswerOptionState
|
|
458
|
+
> {
|
|
459
|
+
state = {deleteFocused: false};
|
|
459
460
|
|
|
460
461
|
handleDeleteBlur = () => {
|
|
461
462
|
this.setState({deleteFocused: false});
|
|
@@ -466,9 +467,8 @@ class AnswerOption extends React.Component<any, any> {
|
|
|
466
467
|
};
|
|
467
468
|
|
|
468
469
|
render(): React.ReactNode {
|
|
469
|
-
let removeButton = null;
|
|
470
|
+
let removeButton: React.ReactNode | null = null;
|
|
470
471
|
if (this.state.deleteFocused) {
|
|
471
|
-
// @ts-expect-error - TS2322 - Type 'Element' is not assignable to type 'null'.
|
|
472
472
|
removeButton = (
|
|
473
473
|
<button
|
|
474
474
|
type="button"
|
|
@@ -480,7 +480,6 @@ class AnswerOption extends React.Component<any, any> {
|
|
|
480
480
|
</button>
|
|
481
481
|
);
|
|
482
482
|
} else {
|
|
483
|
-
// @ts-expect-error - TS2322 - Type 'Element' is not assignable to type 'null'.
|
|
484
483
|
removeButton = (
|
|
485
484
|
<button
|
|
486
485
|
type="button"
|