@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.
@@ -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": "2.16.2",
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": "^17.7.0",
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 {ApiOptions, Dependencies, Widgets} from "@khanacademy/perseus";
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
- describe("input-number widget", () => {
20
- beforeEach(async () => {
21
- const [InputNumberWidget, InputNumberEditor] = await Promise.all([
22
- import("@khanacademy/perseus").then((m) => m.InputNumber),
23
- import("../widgets/input-number-editor").then((m) => m.default),
24
- ]);
25
- Widgets.registerWidgets([InputNumberWidget]);
26
- Widgets.registerEditors([InputNumberEditor]);
27
- jest.useRealTimers();
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
- test("clicking on the widget editor should open it", async () => {
31
- // Arrange
32
- render(
33
- <Editor
34
- apiOptions={ApiOptions.defaults}
35
- content={question1.content}
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
- it("should update values", async () => {
62
- // Arrange
63
- const changeFn = jest.fn();
64
- render(
65
- <Editor
66
- apiOptions={ApiOptions.defaults}
67
- content={question1.content}
68
- placeholder=""
69
- widgets={question1.widgets}
70
- images={question1.images}
71
- disabled={false}
72
- widgetEnabled={true}
73
- immutableWidgets={false}
74
- showWordCount={true}
75
- warnNoPrompt={true}
76
- warnNoWidgets={true}
77
- onChange={changeFn}
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
- undefined,
116
- undefined,
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
+ });
@@ -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, WidgetInfo} from "@khanacademy/perseus";
13
+ import type {Alignment, PerseusWidget} from "@khanacademy/perseus";
14
14
 
15
- const {InlineIcon, PropCheckBox} = components;
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: WidgetInfo,
22
+ widgetInfo: PerseusWidget,
23
23
  cb?: () => unknown,
24
24
  silent?: boolean,
25
25
  ) => unknown;
26
26
  onRemove: () => unknown;
27
27
  apiOptions: any;
28
- } & WidgetInfo;
28
+ } & PerseusWidget;
29
29
 
30
30
  type WidgetEditorState = {
31
31
  showWidget: boolean;
32
- widgetInfo: WidgetInfo;
32
+ widgetInfo: PerseusWidget;
33
33
  };
34
34
 
35
- const _upgradeWidgetInfo = (props: WidgetEditorProps): React.ReactElement => {
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 - TS2322 - Type 'PerseusWidget' is not assignable to type 'ReactElement<any, string | JSXElementConstructor<any>>'. | TS2345 - Argument of type 'Partial<{ id: string; onChange: (widgetInfo: PerseusWidget, cb?: (() => unknown) | undefined, silent?: boolean | undefined) => unknown; onRemove: () => unknown; apiOptions: any; } & CategorizerWidget & { ...; }> | ... 38 more ... | Partial<...>' is not assignable to parameter of type 'PerseusWidget'.
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 WidgetInfo;
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 WidgetInfo;
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 WidgetInfo;
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, WidgetInfo} from "@khanacademy/perseus";
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]: WidgetInfo;
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]: WidgetInfo;
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: (id: string, type: string) => undefined | React.ReactNode =
249
- (id: string, type: string) => {
250
- if (!Widgets.getEditor(type)) {
251
- return;
252
- }
253
- return (
254
- <WidgetEditor
255
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
256
- key={id}
257
- ref={id}
258
- id={id}
259
- // @ts-expect-error - TS2783 - 'type' is specified more than once, so this usage will be overwritten.
260
- type={type}
261
- // eslint-disable-next-line react/jsx-no-bind
262
- onChange={this._handleWidgetEditorChange.bind(this, id)}
263
- // eslint-disable-next-line react/jsx-no-bind
264
- onRemove={this._handleWidgetEditorRemove.bind(this, id)}
265
- apiOptions={this.props.apiOptions}
266
- {...this.props.widgets[id]}
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: WidgetInfo,
271
+ newWidgetInfo: PerseusWidget,
274
272
  cb?: () => unknown,
275
273
  silent?: boolean,
276
274
  ) => void = (
277
275
  id: string,
278
- newWidgetInfo: WidgetInfo,
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
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type '"video" | "image" | "iframe" | "table" | "radio" | "definition" | "group" | "matrix" | "categorizer" | "cs-program" | "dropdown" | "example-graphie-widget" | "example-widget" | ... 26 more ... | "unit-input"'.
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):
@@ -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;