@khanacademy/perseus-editor 2.15.7 → 2.16.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 +16 -0
- package/dist/components/blur-input.d.ts +2 -0
- package/dist/components/widget-editor.d.ts +1 -1
- package/dist/editor-page.d.ts +4 -1
- package/dist/editor.d.ts +3 -1
- package/dist/es/index.js +170 -260
- package/dist/es/index.js.map +1 -1
- package/dist/hint-editor.d.ts +2 -0
- package/dist/iframe-content-renderer.d.ts +1 -0
- package/dist/index.js +137 -256
- package/dist/index.js.map +1 -1
- package/dist/item-editor.d.ts +6 -0
- package/dist/widgets/example-widget-editor.d.ts +1 -0
- package/dist/widgets/example-widget.d.ts +7 -0
- package/dist/widgets/graded-group-editor.d.ts +3 -0
- package/dist/widgets/group-editor.d.ts +2 -0
- package/dist/widgets/input-number-editor.d.ts +2 -0
- package/dist/widgets/simple-markdown-tester-editor.d.ts +7 -0
- package/dist/widgets/table-editor.d.ts +2 -0
- package/package.json +13 -13
- package/src/components/__tests__/blur-input.test.tsx +67 -0
- package/src/components/blur-input.tsx +7 -0
- package/src/editor-page.tsx +11 -25
- package/src/editor.tsx +37 -65
- package/src/hint-editor.tsx +15 -27
- package/src/iframe-content-renderer.tsx +19 -20
- package/src/item-editor.tsx +11 -18
- package/src/util/register-all-widgets-and-editors-for-testing.test.ts +3 -1
- package/src/widgets/example-widget-editor.tsx +4 -5
- package/src/widgets/example-widget.tsx +8 -10
- package/src/widgets/graded-group-editor.tsx +9 -19
- package/src/widgets/group-editor.tsx +5 -8
- package/src/widgets/input-number-editor.tsx +4 -5
- package/src/widgets/simple-markdown-tester-editor.tsx +4 -5
- package/src/widgets/table-editor.tsx +4 -5
- package/tsconfig-build.tsbuildinfo +1 -1
package/dist/item-editor.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import Editor from "./editor";
|
|
3
|
+
import IframeContentRenderer from "./iframe-content-renderer";
|
|
4
|
+
import ItemExtrasEditor from "./item-extras-editor";
|
|
2
5
|
import type { APIOptions, ImageUploader, ChangeHandler } from "@khanacademy/perseus";
|
|
3
6
|
type Props = {
|
|
4
7
|
apiOptions?: APIOptions;
|
|
@@ -18,6 +21,9 @@ declare class ItemEditor extends React.Component<Props> {
|
|
|
18
21
|
onChange: () => void;
|
|
19
22
|
question: Record<any, any>;
|
|
20
23
|
};
|
|
24
|
+
frame: React.RefObject<IframeContentRenderer>;
|
|
25
|
+
questionEditor: React.RefObject<Editor>;
|
|
26
|
+
itemExtrasEditor: React.RefObject<ItemExtrasEditor>;
|
|
21
27
|
updateProps: ChangeHandler;
|
|
22
28
|
triggerPreviewUpdate: (newData?: any) => void;
|
|
23
29
|
handleEditorChange: ChangeHandler;
|
|
@@ -10,6 +10,7 @@ declare class ExampleWidgetEditor extends React.Component<Props> {
|
|
|
10
10
|
};
|
|
11
11
|
static widgetName: "example-widget";
|
|
12
12
|
static defaultProps: Props;
|
|
13
|
+
input: React.RefObject<HTMLInputElement>;
|
|
13
14
|
handleAnswerChange: (arg1: React.ChangeEvent<HTMLInputElement>) => void;
|
|
14
15
|
render(): React.ReactNode;
|
|
15
16
|
change: (arg1: any, arg2: any, arg3: any) => any;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
declare class TextInput extends React.Component<any> {
|
|
4
|
+
input: React.RefObject<HTMLInputElement>;
|
|
5
|
+
render(): React.ReactNode;
|
|
6
|
+
focus: () => boolean;
|
|
7
|
+
changeValue: (e: any) => void;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* This is the widget's renderer. It shows up in the right column
|
|
5
11
|
* in the demo, and is what is visible to users, and where
|
|
@@ -11,6 +17,7 @@ declare class ExampleWidget extends React.Component<any> {
|
|
|
11
17
|
onChange: PropTypes.Validator<(...args: any[]) => any>;
|
|
12
18
|
};
|
|
13
19
|
static defaultProps: any;
|
|
20
|
+
input: React.RefObject<TextInput>;
|
|
14
21
|
/**
|
|
15
22
|
* This is the widget's grading function. simpleValidate generally
|
|
16
23
|
* defers to this function.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import Editor from "../editor";
|
|
3
4
|
type Props = any;
|
|
4
5
|
declare class GradedGroupEditor extends React.Component<Props> {
|
|
5
6
|
static propTypes: {
|
|
@@ -40,6 +41,8 @@ declare class GradedGroupEditor extends React.Component<Props> {
|
|
|
40
41
|
};
|
|
41
42
|
static widgetName: "graded-group";
|
|
42
43
|
static defaultProps: Props;
|
|
44
|
+
editor: React.RefObject<Editor>;
|
|
45
|
+
hintEditor: React.RefObject<Editor>;
|
|
43
46
|
change: (arg1: any, arg2: any, arg3: any) => any;
|
|
44
47
|
handleAddHint: () => void;
|
|
45
48
|
handleRemoveHint: (arg1: React.MouseEvent) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import Editor from "../editor";
|
|
3
4
|
type Props = any;
|
|
4
5
|
declare class GroupEditor extends React.Component<Props> {
|
|
5
6
|
static propTypes: {
|
|
@@ -40,6 +41,7 @@ declare class GroupEditor extends React.Component<Props> {
|
|
|
40
41
|
};
|
|
41
42
|
static widgetName: "group";
|
|
42
43
|
static defaultProps: Props;
|
|
44
|
+
editor: React.RefObject<Editor>;
|
|
43
45
|
render(): React.ReactNode;
|
|
44
46
|
_renderMetadataEditor: () => React.ReactElement;
|
|
45
47
|
change: (arg1: any, arg2: any, arg3: any) => any;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import BlurInput from "../components/blur-input";
|
|
2
3
|
import type { ParsedValue, InputNumber } from "@khanacademy/perseus";
|
|
3
4
|
import type { PropsFor } from "@khanacademy/wonder-blocks-core";
|
|
4
5
|
type Props = {
|
|
@@ -31,6 +32,7 @@ type DefaultProps = {
|
|
|
31
32
|
declare class InputNumberEditor extends React.Component<Props> {
|
|
32
33
|
static widgetName: "input-number";
|
|
33
34
|
static defaultProps: DefaultProps;
|
|
35
|
+
input: React.RefObject<BlurInput>;
|
|
34
36
|
handleAnswerChange: (arg1: string) => void;
|
|
35
37
|
render(): React.ReactNode;
|
|
36
38
|
focus: () => boolean;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
type TextAreaProps = any;
|
|
3
|
+
declare class TextArea extends React.Component<TextAreaProps> {
|
|
4
|
+
render(): React.ReactNode;
|
|
5
|
+
focus: () => boolean;
|
|
6
|
+
changeValue: (e: any) => void;
|
|
7
|
+
}
|
|
2
8
|
type SimpleMarkdownTesterEditorProps = any;
|
|
3
9
|
declare class SimpleMarkdownTesterEditor extends React.Component<SimpleMarkdownTesterEditorProps> {
|
|
4
10
|
static propTypes: {
|
|
@@ -6,6 +12,7 @@ declare class SimpleMarkdownTesterEditor extends React.Component<SimpleMarkdownT
|
|
|
6
12
|
};
|
|
7
13
|
static widgetName: "simple-markdown-tester";
|
|
8
14
|
static defaultProps: SimpleMarkdownTesterEditorProps;
|
|
15
|
+
input: React.RefObject<TextArea>;
|
|
9
16
|
render(): React.ReactNode;
|
|
10
17
|
change: (arg1: any, arg2: any, arg3: any) => any;
|
|
11
18
|
focus: () => boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { components } from "@khanacademy/perseus";
|
|
1
2
|
import PropTypes from "prop-types";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
type Props = any;
|
|
@@ -10,6 +11,7 @@ declare class TableEditor extends React.Component<Props> {
|
|
|
10
11
|
};
|
|
11
12
|
static widgetName: "table";
|
|
12
13
|
static defaultProps: Props;
|
|
14
|
+
numberOfColumns: React.RefObject<components.NumberInput>;
|
|
13
15
|
focus: () => void;
|
|
14
16
|
render(): React.ReactNode;
|
|
15
17
|
onSizeInput: (arg1: number, arg2: number) => void;
|
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.16.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@khanacademy/kas": "^0.3.7",
|
|
26
26
|
"@khanacademy/kmath": "^0.1.8",
|
|
27
|
-
"@khanacademy/math-input": "^16.4.
|
|
28
|
-
"@khanacademy/perseus": "^17.6.
|
|
27
|
+
"@khanacademy/math-input": "^16.4.1",
|
|
28
|
+
"@khanacademy/perseus": "^17.6.1",
|
|
29
29
|
"@khanacademy/perseus-core": "1.4.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@khanacademy/wonder-blocks-button": "^6.2.
|
|
33
|
-
"@khanacademy/wonder-blocks-clickable": "^4.0.
|
|
32
|
+
"@khanacademy/wonder-blocks-button": "^6.2.5",
|
|
33
|
+
"@khanacademy/wonder-blocks-clickable": "^4.0.12",
|
|
34
34
|
"@khanacademy/wonder-blocks-color": "^3.0.0",
|
|
35
|
-
"@khanacademy/wonder-blocks-core": "^6.3.
|
|
35
|
+
"@khanacademy/wonder-blocks-core": "^6.3.1",
|
|
36
36
|
"@khanacademy/wonder-blocks-i18n": "^2.0.2",
|
|
37
|
-
"@khanacademy/wonder-blocks-icon": "4.0.
|
|
37
|
+
"@khanacademy/wonder-blocks-icon": "4.0.1",
|
|
38
38
|
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
|
|
39
|
-
"@khanacademy/wonder-blocks-typography": "^2.1.
|
|
39
|
+
"@khanacademy/wonder-blocks-typography": "^2.1.10",
|
|
40
40
|
"@khanacademy/wonder-stuff-core": "^1.5.1",
|
|
41
41
|
"@phosphor-icons/core": "^2.0.2",
|
|
42
42
|
"aphrodite": "^1.2.5",
|
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
"underscore": "^1.4.4"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@khanacademy/wonder-blocks-button": "^6.2.
|
|
55
|
-
"@khanacademy/wonder-blocks-clickable": "^4.0.
|
|
54
|
+
"@khanacademy/wonder-blocks-button": "^6.2.5",
|
|
55
|
+
"@khanacademy/wonder-blocks-clickable": "^4.0.12",
|
|
56
56
|
"@khanacademy/wonder-blocks-color": "^3.0.0",
|
|
57
|
-
"@khanacademy/wonder-blocks-core": "^6.3.
|
|
57
|
+
"@khanacademy/wonder-blocks-core": "^6.3.1",
|
|
58
58
|
"@khanacademy/wonder-blocks-i18n": "^2.0.2",
|
|
59
|
-
"@khanacademy/wonder-blocks-icon": "4.0.
|
|
59
|
+
"@khanacademy/wonder-blocks-icon": "4.0.1",
|
|
60
60
|
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
|
|
61
|
-
"@khanacademy/wonder-blocks-typography": "^2.1.
|
|
61
|
+
"@khanacademy/wonder-blocks-typography": "^2.1.10",
|
|
62
62
|
"@khanacademy/wonder-stuff-core": "^1.5.1",
|
|
63
63
|
"@phosphor-icons/core": "^2.0.2",
|
|
64
64
|
"aphrodite": "^1.2.5",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {render, screen} from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import BlurInput from "../blur-input";
|
|
6
|
+
|
|
7
|
+
import "@testing-library/jest-dom"; // Imports custom matchers
|
|
8
|
+
|
|
9
|
+
describe("BlurInput", () => {
|
|
10
|
+
it("should render", () => {
|
|
11
|
+
render(<BlurInput value="Hello world!" onChange={() => {}} />);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should call onChange prop on blur", () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
const onChange = jest.fn();
|
|
17
|
+
render(<BlurInput value="Hello world!" onChange={onChange} />);
|
|
18
|
+
|
|
19
|
+
// Act
|
|
20
|
+
userEvent.type(screen.getByRole("textbox"), "Hello Khan Academy!");
|
|
21
|
+
userEvent.tab();
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should call onChange prop when typing into input", () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const onChange = jest.fn();
|
|
30
|
+
render(<BlurInput value="Hello world!" onChange={onChange} />);
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
userEvent.type(screen.getByRole("textbox"), "Hello Khan Academy!");
|
|
34
|
+
// NO TAB
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
expect(onChange).not.toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should call onChange prop when pasting into input", () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const onChange = jest.fn();
|
|
43
|
+
render(<BlurInput value="Hello world!" onChange={onChange} />);
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
userEvent.paste(screen.getByRole("textbox"), "Hello Khan Academy!");
|
|
47
|
+
// NO TAB
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(onChange).not.toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should focus input through focus function", () => {
|
|
54
|
+
const ref = React.createRef<BlurInput>();
|
|
55
|
+
|
|
56
|
+
// Arrange
|
|
57
|
+
render(
|
|
58
|
+
<BlurInput ref={ref} value="Hello world!" onChange={() => {}} />,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
ref.current?.focus();
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(screen.getByRole("textbox")).toHaveFocus();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -27,6 +27,8 @@ type State = {
|
|
|
27
27
|
*/
|
|
28
28
|
// eslint-disable-next-line react/no-unsafe
|
|
29
29
|
class BlurInput extends React.Component<Props, State> {
|
|
30
|
+
input = React.createRef<HTMLInputElement>();
|
|
31
|
+
|
|
30
32
|
constructor(props: Props) {
|
|
31
33
|
super(props);
|
|
32
34
|
this.state = {value: this.props.value};
|
|
@@ -45,9 +47,14 @@ class BlurInput extends React.Component<Props, State> {
|
|
|
45
47
|
this.props.onChange(e.target.value);
|
|
46
48
|
};
|
|
47
49
|
|
|
50
|
+
focus() {
|
|
51
|
+
this.input.current?.focus();
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
render(): React.ReactNode {
|
|
49
55
|
return (
|
|
50
56
|
<input
|
|
57
|
+
ref={this.input}
|
|
51
58
|
className={this.props.className}
|
|
52
59
|
style={this.props.style}
|
|
53
60
|
type="text"
|
package/src/editor-page.tsx
CHANGED
|
@@ -63,10 +63,11 @@ type State = {
|
|
|
63
63
|
|
|
64
64
|
class EditorPage extends React.Component<Props, State> {
|
|
65
65
|
_isMounted: boolean;
|
|
66
|
-
// @ts-expect-error - TS2564 - Property 'rendererMountNode' has no initializer and is not definitely assigned in the constructor.
|
|
67
|
-
rendererMountNode: HTMLDivElement;
|
|
68
66
|
renderer: any;
|
|
69
67
|
|
|
68
|
+
itemEditor = React.createRef<ItemEditor>();
|
|
69
|
+
hintsEditor = React.createRef<CombinedHintsEditor>();
|
|
70
|
+
|
|
70
71
|
static defaultProps: {
|
|
71
72
|
developerMode: boolean;
|
|
72
73
|
jsonMode: boolean;
|
|
@@ -102,7 +103,6 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
102
103
|
// this.isMounted() but is still considered an anti-pattern.
|
|
103
104
|
this._isMounted = true;
|
|
104
105
|
|
|
105
|
-
this.rendererMountNode = document.createElement("div");
|
|
106
106
|
this.updateRenderer();
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -154,9 +154,7 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
154
154
|
isMobile: touch,
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
// @ts-expect-error - TS2339 - Property 'triggerPreviewUpdate' does not exist on type 'ReactInstance'.
|
|
159
|
-
this.refs.itemEditor.triggerPreviewUpdate({
|
|
157
|
+
this.itemEditor.current?.triggerPreviewUpdate({
|
|
160
158
|
type: "question",
|
|
161
159
|
data: _({
|
|
162
160
|
item: this.serialize(),
|
|
@@ -170,9 +168,7 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
170
168
|
paths: this.props.contentPaths || [],
|
|
171
169
|
},
|
|
172
170
|
reviewMode: true,
|
|
173
|
-
|
|
174
|
-
// @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
|
|
175
|
-
legacyPerseusLint: this.refs.itemEditor.getSaveWarnings(),
|
|
171
|
+
legacyPerseusLint: this.itemEditor.current?.getSaveWarnings(),
|
|
176
172
|
}).extend(
|
|
177
173
|
_(this.props).pick(
|
|
178
174
|
"workAreaSelector",
|
|
@@ -192,12 +188,8 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
192
188
|
}
|
|
193
189
|
|
|
194
190
|
getSaveWarnings(): any {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const issues1 = this.refs.itemEditor.getSaveWarnings();
|
|
198
|
-
// eslint-disable-next-line react/no-string-refs
|
|
199
|
-
// @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
|
|
200
|
-
const issues2 = this.refs.hintsEditor.getSaveWarnings();
|
|
191
|
+
const issues1 = this.itemEditor.current?.getSaveWarnings();
|
|
192
|
+
const issues2 = this.hintsEditor.current?.getSaveWarnings();
|
|
201
193
|
return issues1.concat(issues2);
|
|
202
194
|
}
|
|
203
195
|
|
|
@@ -205,12 +197,8 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
205
197
|
if (this.props.jsonMode) {
|
|
206
198
|
return this.state.json;
|
|
207
199
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return _.extend(this.refs.itemEditor.serialize(options), {
|
|
211
|
-
// eslint-disable-next-line react/no-string-refs
|
|
212
|
-
// @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
|
|
213
|
-
hints: this.refs.hintsEditor.serialize(options),
|
|
200
|
+
return _.extend(this.itemEditor.current?.serialize(options), {
|
|
201
|
+
hints: this.hintsEditor.current?.serialize(options),
|
|
214
202
|
});
|
|
215
203
|
}
|
|
216
204
|
|
|
@@ -302,8 +290,7 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
302
290
|
|
|
303
291
|
{(!this.props.developerMode || !this.props.jsonMode) && (
|
|
304
292
|
<ItemEditor
|
|
305
|
-
|
|
306
|
-
ref="itemEditor"
|
|
293
|
+
ref={this.itemEditor}
|
|
307
294
|
itemId={this.props.itemId}
|
|
308
295
|
question={this.props.question}
|
|
309
296
|
answerArea={this.props.answerArea}
|
|
@@ -319,8 +306,7 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
319
306
|
|
|
320
307
|
{(!this.props.developerMode || !this.props.jsonMode) && (
|
|
321
308
|
<CombinedHintsEditor
|
|
322
|
-
|
|
323
|
-
ref="hintsEditor"
|
|
309
|
+
ref={this.hintsEditor}
|
|
324
310
|
itemId={this.props.itemId}
|
|
325
311
|
hints={this.props.hints}
|
|
326
312
|
imageUploader={this.props.imageUploader}
|
package/src/editor.tsx
CHANGED
|
@@ -18,7 +18,6 @@ import katex from "katex";
|
|
|
18
18
|
// eslint-disable-next-line import/no-unassigned-import
|
|
19
19
|
import "./katex-mhchem";
|
|
20
20
|
import * as React from "react";
|
|
21
|
-
import * as ReactDOM from "react-dom";
|
|
22
21
|
import _ from "underscore";
|
|
23
22
|
|
|
24
23
|
import DragTarget from "./components/drag-target";
|
|
@@ -88,8 +87,7 @@ const IMAGE_REGEX = /!\[[^\]]*\]\(([^\s)]+)[^)]*\)/g;
|
|
|
88
87
|
* ignores captures. If you don't need captures, use String::match
|
|
89
88
|
*/
|
|
90
89
|
const allMatches = function (regex: RegExp, str: string) {
|
|
91
|
-
|
|
92
|
-
const result: Array<any | RegExp.matchResult> = [];
|
|
90
|
+
const result: Array<RegExpExecArray> = [];
|
|
93
91
|
// eslint-disable-next-line no-constant-condition
|
|
94
92
|
while (true) {
|
|
95
93
|
const match = regex.exec(str);
|
|
@@ -106,7 +104,7 @@ const allMatches = function (regex: RegExp, str: string) {
|
|
|
106
104
|
* markdown.
|
|
107
105
|
*/
|
|
108
106
|
const imageUrlsFromContent = function (content: string) {
|
|
109
|
-
return
|
|
107
|
+
return allMatches(IMAGE_REGEX, content).map((capture) => capture[1]);
|
|
110
108
|
};
|
|
111
109
|
|
|
112
110
|
type Props = Readonly<{
|
|
@@ -157,6 +155,9 @@ class Editor extends React.Component<Props, State> {
|
|
|
157
155
|
deferredChange: any | null | undefined;
|
|
158
156
|
widgetIds: any | null | undefined;
|
|
159
157
|
|
|
158
|
+
underlay = React.createRef<HTMLDivElement>();
|
|
159
|
+
textarea = React.createRef<HTMLTextAreaElement>();
|
|
160
|
+
|
|
160
161
|
static defaultProps: DefaultProps = {
|
|
161
162
|
content: "",
|
|
162
163
|
placeholder: "",
|
|
@@ -183,11 +184,14 @@ class Editor extends React.Component<Props, State> {
|
|
|
183
184
|
// this.props.onChange during that, since it calls our parent's
|
|
184
185
|
// setState
|
|
185
186
|
this._sizeImages(this.props);
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
187
|
+
// NOTE(jeremy): We use the non-null assertion here (!) because refs
|
|
188
|
+
// are guaranteed to be up-to-date before componentDidMount or
|
|
189
|
+
// componentDidUpdate fires.
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
191
|
+
$(this.textarea.current!)
|
|
189
192
|
// @ts-expect-error - TS2339 - Property 'on' does not exist on type 'JQueryStatic'.
|
|
190
193
|
.on("copy cut", this._maybeCopyWidgets)
|
|
194
|
+
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
191
195
|
.on("paste", this._maybePasteWidgets);
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -200,9 +204,7 @@ class Editor extends React.Component<Props, State> {
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
componentDidUpdate(prevProps: Props) {
|
|
203
|
-
|
|
204
|
-
// eslint-disable-next-line react/no-string-refs
|
|
205
|
-
const textarea = ReactDOM.findDOMNode(this.refs.textarea);
|
|
207
|
+
const textarea = this.textarea.current;
|
|
206
208
|
|
|
207
209
|
// Slightly unorthodox method to ensure that programmatic text changes
|
|
208
210
|
// are in the browser's undo stack.
|
|
@@ -211,20 +213,11 @@ class Editor extends React.Component<Props, State> {
|
|
|
211
213
|
// will return false. However at least in Firefox setting `value` on a
|
|
212
214
|
// textbox clears the undo stack so we don't get unexpected undo
|
|
213
215
|
// behavior.
|
|
214
|
-
if (this.lastUserValue
|
|
215
|
-
/**
|
|
216
|
-
* TODO(somewhatabstract, JIRA-XXXX):
|
|
217
|
-
* textarea should be refined with an instanceof check to
|
|
218
|
-
* HTMLTextAreaElement so that these props are available.
|
|
219
|
-
*/
|
|
220
|
-
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
|
|
216
|
+
if (this.lastUserValue != null && textarea) {
|
|
221
217
|
textarea.focus();
|
|
222
|
-
// @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
|
|
223
218
|
textarea.value = this.lastUserValue;
|
|
224
|
-
// @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
|
|
225
219
|
textarea.selectionStart = 0;
|
|
226
|
-
|
|
227
|
-
textarea.select(0, prevProps.content.length);
|
|
220
|
+
textarea.setSelectionRange(0, prevProps.content.length);
|
|
228
221
|
if (
|
|
229
222
|
document.execCommand(
|
|
230
223
|
"insertText",
|
|
@@ -234,7 +227,6 @@ class Editor extends React.Component<Props, State> {
|
|
|
234
227
|
) {
|
|
235
228
|
// This command is not implemented. Fall back to setting `value`
|
|
236
229
|
// directly.
|
|
237
|
-
// @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
|
|
238
230
|
textarea.value = this.props.content;
|
|
239
231
|
}
|
|
240
232
|
this.lastUserValue = null;
|
|
@@ -293,11 +285,9 @@ class Editor extends React.Component<Props, State> {
|
|
|
293
285
|
};
|
|
294
286
|
|
|
295
287
|
_handleWidgetEditorRemove: (id: string) => void = (id: string) => {
|
|
296
|
-
|
|
297
|
-
const textarea = this.refs.textarea;
|
|
288
|
+
const textarea = this.textarea.current;
|
|
298
289
|
const re = new RegExp(widgetRegExp.replace("{id}", id), "gm");
|
|
299
|
-
|
|
300
|
-
this.props.onChange({content: textarea.value.replace(re, "")});
|
|
290
|
+
this.props.onChange({content: textarea?.value.replace(re, "")});
|
|
301
291
|
};
|
|
302
292
|
|
|
303
293
|
/**
|
|
@@ -450,11 +440,12 @@ class Editor extends React.Component<Props, State> {
|
|
|
450
440
|
// Tab-completion of widgets. For example, to insert an image:
|
|
451
441
|
// type `[[im`, then tab.
|
|
452
442
|
if (e.key === "Tab") {
|
|
453
|
-
//
|
|
454
|
-
|
|
443
|
+
// We're in an event handler attached to the textarea, so the ref
|
|
444
|
+
// can't be empty/undefined! (which is why its safe to use the
|
|
445
|
+
// non-null-assertion here. aka the `!` suffix)
|
|
446
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
447
|
+
const textarea = this.textarea.current!;
|
|
455
448
|
|
|
456
|
-
// findDOMNode can also return Text, but we know it's an element.
|
|
457
|
-
// @ts-expect-error - TS2345 - Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLTextAreaElement'.
|
|
458
449
|
const word = Util.textarea.getWordBeforeCursor(textarea);
|
|
459
450
|
const matches = word.string.toLowerCase().match(shortcutRegexp);
|
|
460
451
|
|
|
@@ -645,9 +636,6 @@ class Editor extends React.Component<Props, State> {
|
|
|
645
636
|
cursorRange: ReadonlyArray<number>,
|
|
646
637
|
widgetType: string,
|
|
647
638
|
) => {
|
|
648
|
-
// eslint-disable-next-line react/no-string-refs
|
|
649
|
-
const textarea = ReactDOM.findDOMNode(this.refs.textarea);
|
|
650
|
-
|
|
651
639
|
// Note: we have to use _.map here instead of Array::map
|
|
652
640
|
// because the results of a .match might be null if no
|
|
653
641
|
// widgets were found.
|
|
@@ -710,11 +698,13 @@ class Editor extends React.Component<Props, State> {
|
|
|
710
698
|
content: newContent,
|
|
711
699
|
widgets: newWidgets,
|
|
712
700
|
},
|
|
713
|
-
|
|
701
|
+
() => {
|
|
702
|
+
if (!this.textarea.current) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
714
706
|
Util.textarea.moveCursor(
|
|
715
|
-
|
|
716
|
-
// @ts-expect-error - TS2345 - Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLTextAreaElement'.
|
|
717
|
-
textarea,
|
|
707
|
+
this.textarea.current,
|
|
718
708
|
// We want to put the cursor after the widget
|
|
719
709
|
// and after any added newlines
|
|
720
710
|
newContent.length - postlude.length,
|
|
@@ -724,21 +714,21 @@ class Editor extends React.Component<Props, State> {
|
|
|
724
714
|
};
|
|
725
715
|
|
|
726
716
|
_addWidget: (widgetType: string) => void = (widgetType: string) => {
|
|
727
|
-
|
|
728
|
-
|
|
717
|
+
const textarea = this.textarea.current;
|
|
718
|
+
if (!textarea) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
729
721
|
|
|
730
722
|
this._addWidgetToContent(
|
|
731
723
|
this.props.content,
|
|
732
|
-
// @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'ReactInstance'. | TS2339 - Property 'selectionEnd' does not exist on type 'ReactInstance'.
|
|
733
724
|
[textarea.selectionStart, textarea.selectionEnd],
|
|
734
725
|
widgetType,
|
|
735
726
|
);
|
|
736
|
-
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
|
|
737
727
|
textarea.focus();
|
|
738
728
|
};
|
|
739
729
|
|
|
740
|
-
addTemplate: (e: React.SyntheticEvent<
|
|
741
|
-
e: React.SyntheticEvent<
|
|
730
|
+
addTemplate: (e: React.SyntheticEvent<HTMLSelectElement>) => void = (
|
|
731
|
+
e: React.SyntheticEvent<HTMLSelectElement>,
|
|
742
732
|
) => {
|
|
743
733
|
const templateType = e.currentTarget.value;
|
|
744
734
|
if (templateType === "") {
|
|
@@ -820,32 +810,17 @@ class Editor extends React.Component<Props, State> {
|
|
|
820
810
|
};
|
|
821
811
|
|
|
822
812
|
focus: () => void = () => {
|
|
823
|
-
|
|
824
|
-
const textarea = ReactDOM.findDOMNode(this.refs.textarea);
|
|
813
|
+
const textarea = this.textarea.current;
|
|
825
814
|
if (textarea) {
|
|
826
|
-
/**
|
|
827
|
-
* TODO(somewhatabstract, JIRA-XXXX):
|
|
828
|
-
* textarea should be refined with an instanceof check to
|
|
829
|
-
* HTMLTextAreaElement so that these props are available.
|
|
830
|
-
*/
|
|
831
|
-
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
|
|
832
815
|
textarea.focus();
|
|
833
816
|
}
|
|
834
817
|
};
|
|
835
818
|
|
|
836
819
|
focusAndMoveToEnd: () => void = () => {
|
|
837
820
|
this.focus();
|
|
838
|
-
|
|
839
|
-
const textarea = ReactDOM.findDOMNode(this.refs.textarea);
|
|
821
|
+
const textarea = this.textarea.current;
|
|
840
822
|
if (textarea) {
|
|
841
|
-
/**
|
|
842
|
-
* TODO(somewhatabstract, JIRA-XXXX):
|
|
843
|
-
* textarea should be refined with an instanceof check to
|
|
844
|
-
* HTMLTextAreaElement so that these props are available.
|
|
845
|
-
*/
|
|
846
|
-
// @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
|
|
847
823
|
textarea.selectionStart = textarea.value.length;
|
|
848
|
-
// @ts-expect-error - TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
|
|
849
824
|
textarea.selectionEnd = textarea.value.length;
|
|
850
825
|
}
|
|
851
826
|
};
|
|
@@ -1003,7 +978,6 @@ class Editor extends React.Component<Props, State> {
|
|
|
1003
978
|
|
|
1004
979
|
const insertTemplateString = "Insert template\u2026";
|
|
1005
980
|
templatesDropDown = (
|
|
1006
|
-
// @ts-expect-error - TS2322 - Type '(e: SyntheticEvent<HTMLTextAreaElement, Event>) => void' is not assignable to type 'ChangeEventHandler<HTMLSelectElement>'.
|
|
1007
981
|
<select onChange={this.addTemplate}>
|
|
1008
982
|
<option value="">{insertTemplateString}</option>
|
|
1009
983
|
<option disabled>--</option>
|
|
@@ -1044,15 +1018,13 @@ class Editor extends React.Component<Props, State> {
|
|
|
1044
1018
|
const completeTextarea = [
|
|
1045
1019
|
<div
|
|
1046
1020
|
className="perseus-textarea-underlay"
|
|
1047
|
-
|
|
1048
|
-
ref="underlay"
|
|
1021
|
+
ref={this.underlay}
|
|
1049
1022
|
key="underlay"
|
|
1050
1023
|
>
|
|
1051
1024
|
{underlayPieces}
|
|
1052
1025
|
</div>,
|
|
1053
1026
|
<textarea
|
|
1054
|
-
|
|
1055
|
-
ref="textarea"
|
|
1027
|
+
ref={this.textarea}
|
|
1056
1028
|
key="textarea"
|
|
1057
1029
|
onChange={this.handleChange}
|
|
1058
1030
|
onKeyDown={this._handleKeyDown}
|