@khanacademy/perseus-editor 2.16.2 → 3.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 +24 -0
- package/dist/components/widget-editor.d.ts +4 -4
- package/dist/editor.d.ts +5 -5
- package/dist/es/index.js +42 -38
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +29 -25
- package/dist/index.js.map +1 -1
- package/dist/widgets/deprecated-standin-editor.d.ts +10 -0
- package/package.json +2 -2
- package/src/__tests__/editor.test.tsx +120 -98
- package/src/__tests__/hint-editor.test.tsx +83 -0
- package/src/all-editors.ts +2 -0
- package/src/components/widget-editor.tsx +10 -22
- package/src/editor.tsx +35 -31
- package/src/hint-editor.tsx +5 -0
- package/src/index.ts +4 -0
- package/src/util/register-all-widgets-and-editors-for-testing.ts +4 -0
- package/src/widgets/deprecated-standin-editor.tsx +29 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type Props = {
|
|
3
|
+
onChange: (options: any) => void;
|
|
4
|
+
};
|
|
5
|
+
declare class DeprecatedStandinEditor extends React.Component<Props> {
|
|
6
|
+
static widgetName: "deprecated-standin";
|
|
7
|
+
serialize(): any;
|
|
8
|
+
render(): React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export default DeprecatedStandinEditor;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Perseus editors",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "3.0.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@khanacademy/kas": "^0.3.7",
|
|
26
26
|
"@khanacademy/kmath": "^0.1.8",
|
|
27
27
|
"@khanacademy/math-input": "^16.5.0",
|
|
28
|
-
"@khanacademy/perseus": "^
|
|
28
|
+
"@khanacademy/perseus": "^18.0.0",
|
|
29
29
|
"@khanacademy/perseus-core": "1.4.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -1,120 +1,142 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ApiOptions,
|
|
3
|
+
Dependencies,
|
|
4
|
+
Widgets,
|
|
5
|
+
widgets,
|
|
6
|
+
Util,
|
|
7
|
+
} from "@khanacademy/perseus";
|
|
2
8
|
import {render, screen} from "@testing-library/react";
|
|
3
9
|
import userEvent from "@testing-library/user-event";
|
|
4
10
|
import * as React from "react";
|
|
5
11
|
import "@testing-library/jest-dom"; // Imports custom mathers
|
|
6
12
|
|
|
7
13
|
import {testDependencies} from "../../../../testing/test-dependencies";
|
|
8
|
-
import {wait} from "../../../../testing/wait";
|
|
9
|
-
import {question1} from "../__testdata__/input-number.testdata";
|
|
10
14
|
import Editor from "../editor";
|
|
15
|
+
import ImageEditor from "../widgets/image-editor";
|
|
16
|
+
|
|
17
|
+
import type {PropsFor} from "@khanacademy/wonder-blocks-core";
|
|
18
|
+
|
|
19
|
+
const Harnessed = (props: Partial<PropsFor<typeof Editor>>) => {
|
|
20
|
+
return (
|
|
21
|
+
<Editor
|
|
22
|
+
apiOptions={ApiOptions.defaults}
|
|
23
|
+
onChange={() => {}}
|
|
24
|
+
content="[[☃ image 1]]"
|
|
25
|
+
widgets={{
|
|
26
|
+
"image 1": {
|
|
27
|
+
type: "image",
|
|
28
|
+
options: {
|
|
29
|
+
backgroundImage: {
|
|
30
|
+
url: "http://placekitten.com/200/300",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
11
39
|
|
|
12
40
|
describe("Editor", () => {
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
43
|
+
const ImageWidget = widgets.find((w) => w.name === "image")!;
|
|
44
|
+
expect(ImageWidget).toBeDefined();
|
|
45
|
+
Widgets.registerWidget("image", ImageWidget);
|
|
46
|
+
Widgets.registerEditors([ImageEditor]);
|
|
47
|
+
});
|
|
48
|
+
|
|
13
49
|
beforeEach(() => {
|
|
14
50
|
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
|
|
15
51
|
testDependencies,
|
|
16
52
|
);
|
|
17
53
|
});
|
|
18
54
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
it("should delete widget if confirmed", async () => {
|
|
56
|
+
const onChangeMock = jest.fn();
|
|
57
|
+
jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
58
|
+
|
|
59
|
+
// Arrange
|
|
60
|
+
render(<Harnessed onChange={onChangeMock} />);
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
userEvent.click(
|
|
64
|
+
screen.getByRole("button", {name: "Remove image widget"}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(onChangeMock).toHaveBeenCalledWith({content: ""});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should NOT delete widget if not confirmed", async () => {
|
|
72
|
+
const onChangeMock = jest.fn();
|
|
73
|
+
jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
74
|
+
|
|
75
|
+
// Arrange
|
|
76
|
+
render(<Harnessed onChange={onChangeMock} />);
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
userEvent.click(
|
|
80
|
+
screen.getByRole("button", {name: "Remove image widget"}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
expect(onChangeMock).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("clicking on the widget editor should open it", async () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
render(<Harnessed />);
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
const widgetDisclosure = screen.getByRole("link", {
|
|
93
|
+
name: "image 1",
|
|
28
94
|
});
|
|
95
|
+
userEvent.click(widgetDisclosure);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
const previewImage = screen.getByAltText("Editor preview of image");
|
|
99
|
+
expect(previewImage).toHaveAttribute(
|
|
100
|
+
"src",
|
|
101
|
+
"http://placekitten.com/200/300",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should update values", async () => {
|
|
106
|
+
// Arrange
|
|
107
|
+
jest.spyOn(Util, "getImageSizeModern").mockResolvedValue([200, 200]);
|
|
29
108
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
placeholder=""
|
|
37
|
-
widgets={question1.widgets}
|
|
38
|
-
images={question1.images}
|
|
39
|
-
disabled={false}
|
|
40
|
-
widgetEnabled={true}
|
|
41
|
-
immutableWidgets={false}
|
|
42
|
-
showWordCount={true}
|
|
43
|
-
warnNoPrompt={true}
|
|
44
|
-
warnNoWidgets={true}
|
|
45
|
-
onChange={(props) => {}}
|
|
46
|
-
/>,
|
|
47
|
-
);
|
|
48
|
-
await wait();
|
|
49
|
-
|
|
50
|
-
// Act
|
|
51
|
-
const widgetDisclosure = screen.getByRole("link", {
|
|
52
|
-
name: "input-number 1",
|
|
53
|
-
});
|
|
54
|
-
userEvent.click(widgetDisclosure);
|
|
55
|
-
const correctAnswerInput = screen.getByLabelText("Correct answer:");
|
|
56
|
-
|
|
57
|
-
// Assert
|
|
58
|
-
expect(correctAnswerInput).toHaveValue("0.5");
|
|
109
|
+
const changeFn = jest.fn();
|
|
110
|
+
render(<Harnessed onChange={changeFn} />);
|
|
111
|
+
|
|
112
|
+
// Act
|
|
113
|
+
const widgetDisclosure = screen.getByRole("link", {
|
|
114
|
+
name: "image 1",
|
|
59
115
|
});
|
|
116
|
+
userEvent.click(widgetDisclosure);
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
await wait();
|
|
81
|
-
|
|
82
|
-
// Act
|
|
83
|
-
const widgetDisclosure = screen.getByRole("link", {
|
|
84
|
-
name: "input-number 1",
|
|
85
|
-
});
|
|
86
|
-
userEvent.click(widgetDisclosure);
|
|
87
|
-
const correctAnswerInput = screen.getByLabelText("Correct answer:");
|
|
88
|
-
|
|
89
|
-
userEvent.clear(correctAnswerInput);
|
|
90
|
-
userEvent.paste(correctAnswerInput, "0.75");
|
|
91
|
-
userEvent.tab(); // blurring the input triggers onChange to be called
|
|
92
|
-
|
|
93
|
-
// Assert
|
|
94
|
-
expect(changeFn).toHaveBeenCalledWith(
|
|
95
|
-
{
|
|
96
|
-
widgets: {
|
|
97
|
-
"input-number 1": {
|
|
98
|
-
graded: true,
|
|
99
|
-
version: {major: 0, minor: 0},
|
|
100
|
-
static: false,
|
|
101
|
-
type: "input-number",
|
|
102
|
-
options: {
|
|
103
|
-
value: 0.75,
|
|
104
|
-
simplify: "required",
|
|
105
|
-
size: "normal",
|
|
106
|
-
inexact: false,
|
|
107
|
-
maxError: 0.1,
|
|
108
|
-
answerType: "number",
|
|
109
|
-
rightAlign: false,
|
|
110
|
-
},
|
|
111
|
-
alignment: "default",
|
|
112
|
-
},
|
|
113
|
-
},
|
|
118
|
+
const captionInput = screen.getByLabelText(/Caption:/);
|
|
119
|
+
|
|
120
|
+
userEvent.clear(captionInput);
|
|
121
|
+
userEvent.type(captionInput, "A picture of kittens");
|
|
122
|
+
userEvent.tab(); // blurring the input triggers onChange to be called
|
|
123
|
+
jest.runOnlyPendingTimers();
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
expect(changeFn).toHaveBeenCalledWith(
|
|
127
|
+
{
|
|
128
|
+
widgets: {
|
|
129
|
+
"image 1": expect.objectContaining({
|
|
130
|
+
type: "image",
|
|
131
|
+
graded: true,
|
|
132
|
+
options: expect.objectContaining({
|
|
133
|
+
caption: "A picture of kittens",
|
|
134
|
+
}),
|
|
135
|
+
}),
|
|
114
136
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
},
|
|
138
|
+
undefined,
|
|
139
|
+
undefined,
|
|
140
|
+
);
|
|
119
141
|
});
|
|
120
142
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {render, screen} from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import CombinedHintsEditor from "../hint-editor";
|
|
7
|
+
|
|
8
|
+
describe("CombinedHintsEditor", () => {
|
|
9
|
+
it("should render", () => {
|
|
10
|
+
render(
|
|
11
|
+
<CombinedHintsEditor deviceType="phone" previewURL="about:blank" />,
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should confirm before removing a hint", () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const comfirmSpy = jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
18
|
+
render(
|
|
19
|
+
<CombinedHintsEditor
|
|
20
|
+
deviceType="phone"
|
|
21
|
+
previewURL="about:blank"
|
|
22
|
+
hints={[
|
|
23
|
+
{content: "You know this one!", widgets: {}, images: {}},
|
|
24
|
+
{content: "Ok, the answer is 3", widgets: {}, images: {}},
|
|
25
|
+
]}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
userEvent.click(screen.getAllByText("Remove this hint")[0]);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(comfirmSpy).toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should not remove the hint if not confirmed", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
39
|
+
const onChangeMock = jest.fn();
|
|
40
|
+
render(
|
|
41
|
+
<CombinedHintsEditor
|
|
42
|
+
deviceType="phone"
|
|
43
|
+
previewURL="about:blank"
|
|
44
|
+
hints={[
|
|
45
|
+
{content: "You know this one!", widgets: {}, images: {}},
|
|
46
|
+
{content: "Ok, the answer is 3", widgets: {}, images: {}},
|
|
47
|
+
]}
|
|
48
|
+
onChange={onChangeMock}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
userEvent.click(screen.getAllByText("Remove this hint")[0]);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(onChangeMock).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should remove the hint if confirmed", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
62
|
+
const onChangeMock = jest.fn();
|
|
63
|
+
render(
|
|
64
|
+
<CombinedHintsEditor
|
|
65
|
+
deviceType="phone"
|
|
66
|
+
previewURL="about:blank"
|
|
67
|
+
hints={[
|
|
68
|
+
{content: "You know this one!", widgets: {}, images: {}},
|
|
69
|
+
{content: "Ok, the answer is 3", widgets: {}, images: {}},
|
|
70
|
+
]}
|
|
71
|
+
onChange={onChangeMock}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
userEvent.click(screen.getAllByText("Remove this hint")[0]);
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(onChangeMock).toHaveBeenCalledWith({
|
|
80
|
+
hints: [{content: "Ok, the answer is 3", widgets: {}, images: {}}],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/all-editors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import CategorizerEditor from "./widgets/categorizer-editor";
|
|
2
2
|
import CSProgramEditor from "./widgets/cs-program-editor";
|
|
3
3
|
import DefinitionEditor from "./widgets/definition-editor";
|
|
4
|
+
import DeprecatedStandinEditor from "./widgets/deprecated-standin-editor";
|
|
4
5
|
import DropdownEditor from "./widgets/dropdown-editor";
|
|
5
6
|
import ExampleGraphieWidgetEditor from "./widgets/example-graphie-widget-editor";
|
|
6
7
|
import ExampleWidgetEditor from "./widgets/example-widget-editor";
|
|
@@ -80,4 +81,5 @@ export default [
|
|
|
80
81
|
UnitEditor,
|
|
81
82
|
VideoEditor,
|
|
82
83
|
RadioEditor,
|
|
84
|
+
DeprecatedStandinEditor,
|
|
83
85
|
];
|
|
@@ -10,33 +10,33 @@ import _ from "underscore";
|
|
|
10
10
|
|
|
11
11
|
import SectionControlButton from "./section-control-button";
|
|
12
12
|
|
|
13
|
-
import type {Alignment,
|
|
13
|
+
import type {Alignment, PerseusWidget} from "@khanacademy/perseus";
|
|
14
14
|
|
|
15
|
-
const {InlineIcon
|
|
15
|
+
const {InlineIcon} = components;
|
|
16
16
|
const {iconChevronDown, iconChevronRight, iconTrash} = icons;
|
|
17
17
|
|
|
18
18
|
type WidgetEditorProps = {
|
|
19
19
|
// Unserialized props
|
|
20
20
|
id: string;
|
|
21
21
|
onChange: (
|
|
22
|
-
widgetInfo:
|
|
22
|
+
widgetInfo: PerseusWidget,
|
|
23
23
|
cb?: () => unknown,
|
|
24
24
|
silent?: boolean,
|
|
25
25
|
) => unknown;
|
|
26
26
|
onRemove: () => unknown;
|
|
27
27
|
apiOptions: any;
|
|
28
|
-
} &
|
|
28
|
+
} & PerseusWidget;
|
|
29
29
|
|
|
30
30
|
type WidgetEditorState = {
|
|
31
31
|
showWidget: boolean;
|
|
32
|
-
widgetInfo:
|
|
32
|
+
widgetInfo: PerseusWidget;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const _upgradeWidgetInfo = (props: WidgetEditorProps):
|
|
35
|
+
const _upgradeWidgetInfo = (props: WidgetEditorProps): PerseusWidget => {
|
|
36
36
|
// We can't call serialize here because this.refs.widget
|
|
37
37
|
// doesn't exist before this component is mounted.
|
|
38
38
|
const filteredProps = _.omit(props, WIDGET_PROP_DENYLIST);
|
|
39
|
-
// @ts-expect-error
|
|
39
|
+
// @ts-expect-error TS(2345) Type '"categorizer" | undefined' is not assignable to type '"deprecated-standin"'.
|
|
40
40
|
return Widgets.upgradeWidgetInfoToLatestVersion(filteredProps);
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -53,14 +53,12 @@ class WidgetEditor extends React.Component<
|
|
|
53
53
|
super(props);
|
|
54
54
|
this.state = {
|
|
55
55
|
showWidget: false,
|
|
56
|
-
// @ts-expect-error - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
|
|
57
56
|
widgetInfo: _upgradeWidgetInfo(props),
|
|
58
57
|
};
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
// eslint-disable-next-line react/no-unsafe
|
|
62
61
|
UNSAFE_componentWillReceiveProps(nextProps: WidgetEditorProps) {
|
|
63
|
-
// @ts-expect-error - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
|
|
64
62
|
this.setState({widgetInfo: _upgradeWidgetInfo(nextProps)});
|
|
65
63
|
}
|
|
66
64
|
|
|
@@ -77,7 +75,7 @@ class WidgetEditor extends React.Component<
|
|
|
77
75
|
const newWidgetInfo = Object.assign(
|
|
78
76
|
{},
|
|
79
77
|
this.state.widgetInfo,
|
|
80
|
-
) as
|
|
78
|
+
) as PerseusWidget;
|
|
81
79
|
newWidgetInfo.options = Object.assign(
|
|
82
80
|
// eslint-disable-next-line react/no-string-refs
|
|
83
81
|
// @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
|
|
@@ -91,7 +89,7 @@ class WidgetEditor extends React.Component<
|
|
|
91
89
|
e.preventDefault();
|
|
92
90
|
const newWidgetInfo = Object.assign({}, this.state.widgetInfo, {
|
|
93
91
|
static: !this.state.widgetInfo.static,
|
|
94
|
-
}) as
|
|
92
|
+
}) as PerseusWidget;
|
|
95
93
|
this.props.onChange(newWidgetInfo);
|
|
96
94
|
};
|
|
97
95
|
|
|
@@ -100,7 +98,7 @@ class WidgetEditor extends React.Component<
|
|
|
100
98
|
const newWidgetInfo = Object.assign(
|
|
101
99
|
{},
|
|
102
100
|
this.state.widgetInfo,
|
|
103
|
-
) as
|
|
101
|
+
) as PerseusWidget;
|
|
104
102
|
newWidgetInfo.alignment = newAlignment;
|
|
105
103
|
this.props.onChange(newWidgetInfo);
|
|
106
104
|
};
|
|
@@ -145,15 +143,6 @@ class WidgetEditor extends React.Component<
|
|
|
145
143
|
|
|
146
144
|
const supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type);
|
|
147
145
|
|
|
148
|
-
const isUngradedEnabled = widgetInfo.type === "transformer";
|
|
149
|
-
const gradedPropBox = (
|
|
150
|
-
<PropCheckBox
|
|
151
|
-
label="Graded:"
|
|
152
|
-
graded={widgetInfo.graded}
|
|
153
|
-
onChange={this.props.onChange}
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
|
|
157
146
|
return (
|
|
158
147
|
<div className="perseus-widget-editor">
|
|
159
148
|
<div
|
|
@@ -214,7 +203,6 @@ class WidgetEditor extends React.Component<
|
|
|
214
203
|
(this.state.showWidget ? "enter" : "leave")
|
|
215
204
|
}
|
|
216
205
|
>
|
|
217
|
-
{isUngradedEnabled && gradedPropBox}
|
|
218
206
|
{Ed && (
|
|
219
207
|
<Ed
|
|
220
208
|
// eslint-disable-next-line react/no-string-refs
|
package/src/editor.tsx
CHANGED
|
@@ -25,7 +25,7 @@ import WidgetEditor from "./components/widget-editor";
|
|
|
25
25
|
import WidgetSelect from "./components/widget-select";
|
|
26
26
|
import TexErrorView from "./tex-error-view";
|
|
27
27
|
|
|
28
|
-
import type {ChangeHandler,
|
|
28
|
+
import type {ChangeHandler, PerseusWidget} from "@khanacademy/perseus";
|
|
29
29
|
|
|
30
30
|
// like [[snowman input-number 1]]
|
|
31
31
|
const widgetPlaceholder = "[[\u2603 {id}]]";
|
|
@@ -114,7 +114,7 @@ type Props = Readonly<{
|
|
|
114
114
|
replace?: any;
|
|
115
115
|
placeholder: string;
|
|
116
116
|
widgets: {
|
|
117
|
-
[name: string]:
|
|
117
|
+
[name: string]: PerseusWidget;
|
|
118
118
|
};
|
|
119
119
|
images: any;
|
|
120
120
|
disabled: boolean;
|
|
@@ -141,7 +141,7 @@ type DefaultProps = {
|
|
|
141
141
|
warnNoWidgets: boolean;
|
|
142
142
|
widgetEnabled: boolean;
|
|
143
143
|
widgets: {
|
|
144
|
-
[name: string]:
|
|
144
|
+
[name: string]: PerseusWidget;
|
|
145
145
|
};
|
|
146
146
|
};
|
|
147
147
|
|
|
@@ -245,37 +245,35 @@ class Editor extends React.Component<Props, State> {
|
|
|
245
245
|
clearTimeout(this.deferredChange);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
getWidgetEditor
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
);
|
|
269
|
-
};
|
|
248
|
+
getWidgetEditor(
|
|
249
|
+
id: string,
|
|
250
|
+
type: PerseusWidget["type"],
|
|
251
|
+
): undefined | React.ReactNode {
|
|
252
|
+
if (!Widgets.getEditor(type)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
return (
|
|
256
|
+
<WidgetEditor
|
|
257
|
+
ref={id}
|
|
258
|
+
id={id}
|
|
259
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
260
|
+
onChange={this._handleWidgetEditorChange.bind(this, id)}
|
|
261
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
262
|
+
onRemove={this._handleWidgetEditorRemove.bind(this, id)}
|
|
263
|
+
apiOptions={this.props.apiOptions}
|
|
264
|
+
{...this.props.widgets[id]}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
270
268
|
|
|
271
269
|
_handleWidgetEditorChange: (
|
|
272
270
|
id: string,
|
|
273
|
-
newWidgetInfo:
|
|
271
|
+
newWidgetInfo: PerseusWidget,
|
|
274
272
|
cb?: () => unknown,
|
|
275
273
|
silent?: boolean,
|
|
276
274
|
) => void = (
|
|
277
275
|
id: string,
|
|
278
|
-
newWidgetInfo:
|
|
276
|
+
newWidgetInfo: PerseusWidget,
|
|
279
277
|
cb?: () => unknown,
|
|
280
278
|
silent?: boolean,
|
|
281
279
|
) => {
|
|
@@ -285,6 +283,11 @@ class Editor extends React.Component<Props, State> {
|
|
|
285
283
|
};
|
|
286
284
|
|
|
287
285
|
_handleWidgetEditorRemove: (id: string) => void = (id: string) => {
|
|
286
|
+
// eslint-disable-next-line no-alert
|
|
287
|
+
if (!confirm("Are you sure you want to delete this item?")) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
288
291
|
const textarea = this.textarea.current;
|
|
289
292
|
const re = new RegExp(widgetRegExp.replace("{id}", id), "gm");
|
|
290
293
|
this.props.onChange({content: textarea?.value.replace(re, "")});
|
|
@@ -680,14 +683,13 @@ class Editor extends React.Component<Props, State> {
|
|
|
680
683
|
const newContent = newPrelude + widgetContent + newPostlude;
|
|
681
684
|
|
|
682
685
|
const newWidgets = _.clone(this.props.widgets);
|
|
686
|
+
// @ts-expect-error TS(2345) Type '"categorizer" | undefined' is not assignable to type '"deprecated-standin"'.
|
|
683
687
|
newWidgets[id] = {
|
|
684
688
|
options: Widgets.getEditor(widgetType)?.defaultProps,
|
|
685
|
-
|
|
686
|
-
type: widgetType,
|
|
689
|
+
type: widgetType as PerseusWidget["type"],
|
|
687
690
|
// Track widget version on creation, so that a widget editor
|
|
688
691
|
// without a valid version prop can only possibly refer to a
|
|
689
692
|
// pre-versioning creation time.
|
|
690
|
-
// @ts-expect-error - TS2322 - Type 'Version | null | undefined' is not assignable to type 'Version | undefined'.
|
|
691
693
|
version: Widgets.getVersion(widgetType),
|
|
692
694
|
};
|
|
693
695
|
|
|
@@ -905,6 +907,8 @@ class Editor extends React.Component<Props, State> {
|
|
|
905
907
|
underlayPieces = [];
|
|
906
908
|
|
|
907
909
|
for (let i = 0; i < pieces.length; i++) {
|
|
910
|
+
// We split on widgets so every even-numbered indexed piece is
|
|
911
|
+
// text and odd-numbered indexes are the widget references.
|
|
908
912
|
if (i % 2 === 0) {
|
|
909
913
|
// Normal text
|
|
910
914
|
underlayPieces.push(pieces[i]);
|
|
@@ -933,7 +937,7 @@ class Editor extends React.Component<Props, State> {
|
|
|
933
937
|
const match = Util.rWidgetParts.exec(pieces[i]);
|
|
934
938
|
if (match != null) {
|
|
935
939
|
const id = match[1];
|
|
936
|
-
const type = match[2];
|
|
940
|
+
const type = match[2] as PerseusWidget["type"];
|
|
937
941
|
|
|
938
942
|
const selected = false;
|
|
939
943
|
// TODO(alpert):
|
package/src/hint-editor.tsx
CHANGED
|
@@ -334,6 +334,11 @@ class CombinedHintsEditor extends React.Component<CombinedHintsEditorProps> {
|
|
|
334
334
|
};
|
|
335
335
|
|
|
336
336
|
handleHintRemove: (i: number) => void = (i: number) => {
|
|
337
|
+
// eslint-disable-next-line no-alert
|
|
338
|
+
if (!confirm("Are you sure you want to delete this hint?")) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
337
342
|
const hints = _(this.props.hints).clone();
|
|
338
343
|
// @ts-expect-error - TS2339 - Property 'splice' does not exist on type 'Hint'.
|
|
339
344
|
hints.splice(i, 1);
|
package/src/index.ts
CHANGED
|
@@ -24,4 +24,8 @@ import AllWidgets from "./all-widgets";
|
|
|
24
24
|
Widgets.registerEditors(AllEditors);
|
|
25
25
|
Widgets.registerWidgets(AllWidgets);
|
|
26
26
|
|
|
27
|
+
// When it's time to ship transformer's deprecation uncomment
|
|
28
|
+
// Widgets.replaceEditor("transformer", "deprecated-standin");
|
|
29
|
+
// Widgets.replaceWidget("transformer", "deprecatd-standin");
|
|
30
|
+
|
|
27
31
|
export {AllEditors, AllWidgets};
|
|
@@ -11,4 +11,8 @@ import allWidgets from "../all-widgets";
|
|
|
11
11
|
export const registerAllWidgetsAndEditorsForTesting = () => {
|
|
12
12
|
Widgets.registerWidgets(allWidgets);
|
|
13
13
|
Widgets.registerEditors(allEditors);
|
|
14
|
+
|
|
15
|
+
// When it's time to ship transformer's deprecation uncomment
|
|
16
|
+
// Widgets.replaceEditor("transformer", "deprecated-standin");
|
|
17
|
+
// Widgets.replaceWidget("transformer", "deprecatd-standin");
|
|
14
18
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {EditorJsonify} from "@khanacademy/perseus";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
// Callback for when a widget prop is changed.
|
|
6
|
+
onChange: (options: any) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
class DeprecatedStandinEditor extends React.Component<Props> {
|
|
10
|
+
static widgetName = "deprecated-standin" as const;
|
|
11
|
+
|
|
12
|
+
serialize(): any {
|
|
13
|
+
return EditorJsonify.serialize.call(this);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render(): React.ReactNode {
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<p>This widget has been deprecated and removed</p>
|
|
20
|
+
<p>
|
|
21
|
+
Learners will see a message and they will not be graded on
|
|
22
|
+
this part. Please replace this widget with a supported one.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default DeprecatedStandinEditor;
|