@khanacademy/perseus-editor 2.2.2 → 2.3.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 +22 -0
- package/dist/all-widgets.d.ts +2 -2
- package/dist/all-widgets.js.flow +2 -2
- package/dist/components/graph-points-count-selector.d.ts +7 -0
- package/dist/components/graph-points-count-selector.js.flow +12 -0
- package/dist/components/graph-type-selector.d.ts +7 -0
- package/dist/components/graph-type-selector.js.flow +12 -0
- package/dist/components/segment-count-selector.d.ts +6 -0
- package/dist/components/segment-count-selector.js.flow +11 -0
- package/dist/components/widget-editor.d.ts +31 -0
- package/dist/components/widget-editor.js.flow +91 -0
- package/dist/components/widget-select.d.ts +10 -0
- package/dist/components/widget-select.js.flow +16 -0
- package/dist/es/index.js +423 -229
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +431 -225
- package/dist/index.js.map +1 -1
- package/dist/testing-widgets.d.ts +2 -2
- package/dist/testing-widgets.js.flow +2 -2
- package/dist/util/points.d.ts +3 -0
- package/dist/util/points.js.flow +9 -0
- package/dist/widgets/example-graphie-widget.d.ts +2 -2
- package/dist/widgets/example-graphie-widget.js.flow +2 -2
- package/dist/widgets/example-widget.d.ts +2 -2
- package/dist/widgets/example-widget.js.flow +2 -2
- package/dist/widgets/interactive-graph-editor.d.ts +3182 -1
- package/dist/widgets/interactive-graph-editor.js.flow +5290 -1
- package/package.json +9 -3
- package/src/__stories__/editor.stories.tsx +105 -4
- package/src/article-editor.tsx +1 -1
- package/src/{__stories__ → components/__stories__}/section-control-button.stories.tsx +1 -1
- package/src/components/graph-points-count-selector.tsx +29 -0
- package/src/components/graph-type-selector.tsx +31 -0
- package/src/components/json-editor.tsx +0 -1
- package/src/components/segment-count-selector.tsx +27 -0
- package/src/components/widget-editor.tsx +232 -0
- package/src/components/widget-select.tsx +48 -0
- package/src/editor.tsx +4 -270
- package/src/util/points.test.ts +24 -0
- package/src/util/points.ts +13 -0
- package/src/widgets/interactive-graph-editor.tsx +294 -48
- package/tsconfig-build.json +0 -1
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/diffs/__stories__/perseus-diff-wrapper.d.ts +0 -6
- package/dist/diffs/__stories__/perseus-diff-wrapper.js.flow +0 -12
- /package/dist/{section-control-button.d.ts → components/section-control-button.d.ts} +0 -0
- /package/dist/{section-control-button.js.flow → components/section-control-button.js.flow} +0 -0
- /package/src/{section-control-button.tsx → components/section-control-button.tsx} +0 -0
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.3.1",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@khanacademy/kas": "^0.3.1",
|
|
25
25
|
"@khanacademy/kmath": "^0.1.1",
|
|
26
|
-
"@khanacademy/perseus": "^6.
|
|
26
|
+
"@khanacademy/perseus": "^6.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@khanacademy/wonder-blocks-button": "^4.0.8",
|
|
30
30
|
"@khanacademy/wonder-blocks-clickable": "^3.0.8",
|
|
31
31
|
"@khanacademy/wonder-blocks-color": "^2.0.1",
|
|
32
|
+
"@khanacademy/wonder-blocks-core": "^5.0.4",
|
|
32
33
|
"@khanacademy/wonder-blocks-i18n": "^2.0.2",
|
|
34
|
+
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
|
|
35
|
+
"@khanacademy/wonder-blocks-typography": "^2.0.8",
|
|
33
36
|
"aphrodite": "^1.2.5",
|
|
34
37
|
"classnames": "^1.1.4",
|
|
35
38
|
"create-react-class": "^15.6.3",
|
|
@@ -45,7 +48,10 @@
|
|
|
45
48
|
"@khanacademy/wonder-blocks-button": "^4.0.8",
|
|
46
49
|
"@khanacademy/wonder-blocks-clickable": "^3.0.8",
|
|
47
50
|
"@khanacademy/wonder-blocks-color": "^2.0.1",
|
|
51
|
+
"@khanacademy/wonder-blocks-core": "^5.0.4",
|
|
48
52
|
"@khanacademy/wonder-blocks-i18n": "^2.0.2",
|
|
53
|
+
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
|
|
54
|
+
"@khanacademy/wonder-blocks-typography": "^2.0.8",
|
|
49
55
|
"aphrodite": "^1.2.5",
|
|
50
56
|
"classnames": "^1.1.4",
|
|
51
57
|
"create-react-class": "^15.6.3",
|
|
@@ -56,4 +62,4 @@
|
|
|
56
62
|
"underscore": "^1.4.4"
|
|
57
63
|
},
|
|
58
64
|
"keywords": []
|
|
59
|
-
}
|
|
65
|
+
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import {ApiOptions, PerseusRenderer} from "@khanacademy/perseus";
|
|
3
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
4
|
+
import {action} from "@storybook/addon-actions";
|
|
2
5
|
import * as React from "react";
|
|
3
6
|
|
|
7
|
+
import SideBySide from "../../../../testing/side-by-side";
|
|
4
8
|
import {question1} from "../__testdata__/input-number.testdata";
|
|
5
9
|
import Editor from "../editor";
|
|
6
10
|
import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";
|
|
7
11
|
|
|
12
|
+
registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry:
|
|
13
|
+
|
|
8
14
|
export default {
|
|
9
15
|
title: "Perseus/Editor",
|
|
10
16
|
};
|
|
11
17
|
|
|
12
|
-
export const
|
|
13
|
-
registerAllWidgetsAndEditorsForTesting();
|
|
14
|
-
|
|
18
|
+
export const Demo = (): React.ReactElement => {
|
|
15
19
|
return (
|
|
16
20
|
<Editor
|
|
17
21
|
apiOptions={ApiOptions.defaults}
|
|
@@ -29,3 +33,100 @@ export const Rational = (): React.ReactElement => {
|
|
|
29
33
|
/>
|
|
30
34
|
);
|
|
31
35
|
};
|
|
36
|
+
|
|
37
|
+
export const DemoInteractiveGraph = (): React.ReactElement => {
|
|
38
|
+
const editorRef = React.useRef<Editor>(null);
|
|
39
|
+
const [options, setOptions] = React.useState({});
|
|
40
|
+
const [content, setContent] = React.useState(
|
|
41
|
+
"[[\u2603 interactive-graph 1]]",
|
|
42
|
+
);
|
|
43
|
+
const [images, setImages] = React.useState<PerseusRenderer["images"]>({});
|
|
44
|
+
const [widgets, setWidgets] = React.useState<PerseusRenderer["widgets"]>({
|
|
45
|
+
"interactive-graph 1": {
|
|
46
|
+
options: {
|
|
47
|
+
labels: ["x", "y"],
|
|
48
|
+
range: [
|
|
49
|
+
[-10, 10],
|
|
50
|
+
[-10, 10],
|
|
51
|
+
],
|
|
52
|
+
gridStep: [1, 1],
|
|
53
|
+
snapStep: [1, 1],
|
|
54
|
+
step: [1, 1],
|
|
55
|
+
backgroundImage: {
|
|
56
|
+
url: null,
|
|
57
|
+
},
|
|
58
|
+
markings: "graph",
|
|
59
|
+
showProtractor: false,
|
|
60
|
+
showRuler: false,
|
|
61
|
+
showTooltips: false,
|
|
62
|
+
rulerLabel: "",
|
|
63
|
+
rulerTicks: 10,
|
|
64
|
+
graph: {
|
|
65
|
+
type: "linear",
|
|
66
|
+
coords: [
|
|
67
|
+
[1, 1],
|
|
68
|
+
[5, 5],
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
correct: {
|
|
72
|
+
type: "linear",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
type: "interactive-graph",
|
|
76
|
+
version: {
|
|
77
|
+
major: 0,
|
|
78
|
+
minor: 0,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
// Many of the editor components use scoped CSS that requires this
|
|
85
|
+
// class to be above it.
|
|
86
|
+
// TODO: Refactor to aphrodite styles instead of scoped CSS in Less.
|
|
87
|
+
<div className="framework-perseus">
|
|
88
|
+
<SideBySide
|
|
89
|
+
leftTitle="Editor"
|
|
90
|
+
left={
|
|
91
|
+
<View style={{width: "360px", margin: "20px"}}>
|
|
92
|
+
<Editor
|
|
93
|
+
ref={editorRef}
|
|
94
|
+
apiOptions={ApiOptions.defaults}
|
|
95
|
+
content={content}
|
|
96
|
+
placeholder=""
|
|
97
|
+
widgets={widgets}
|
|
98
|
+
images={images}
|
|
99
|
+
disabled={false}
|
|
100
|
+
widgetEnabled={true}
|
|
101
|
+
immutableWidgets={false}
|
|
102
|
+
showWordCount={true}
|
|
103
|
+
warnNoPrompt={false}
|
|
104
|
+
warnNoWidgets={true}
|
|
105
|
+
onChange={(props: Partial<PerseusRenderer>) => {
|
|
106
|
+
action("onChange")(props);
|
|
107
|
+
if (props.content) {
|
|
108
|
+
setContent(props.content);
|
|
109
|
+
} else if (props.widgets) {
|
|
110
|
+
setWidgets(props.widgets);
|
|
111
|
+
} else if (props.images) {
|
|
112
|
+
setImages(props.images);
|
|
113
|
+
}
|
|
114
|
+
// We need to wait for one tick so that the editor
|
|
115
|
+
// has been re-rendered with the changed props. If
|
|
116
|
+
// we don't wait, we get the values from the n-1
|
|
117
|
+
// render and miss the latest change.
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
setOptions(
|
|
120
|
+
editorRef.current?.serialize() || {},
|
|
121
|
+
);
|
|
122
|
+
}, 0);
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
</View>
|
|
126
|
+
}
|
|
127
|
+
rightTitle="Serialized Widget Options"
|
|
128
|
+
jsonObject={options}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
package/src/article-editor.tsx
CHANGED
|
@@ -17,9 +17,9 @@ import _ from "underscore";
|
|
|
17
17
|
|
|
18
18
|
import DeviceFramer from "./components/device-framer";
|
|
19
19
|
import JsonEditor from "./components/json-editor";
|
|
20
|
+
import SectionControlButton from "./components/section-control-button";
|
|
20
21
|
import Editor from "./editor";
|
|
21
22
|
import IframeContentRenderer from "./iframe-content-renderer";
|
|
22
|
-
import SectionControlButton from "./section-control-button";
|
|
23
23
|
|
|
24
24
|
import type {APIOptions} from "@khanacademy/perseus";
|
|
25
25
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import {PointValue, UNLIMITED, parsePointCount} from "../util/points";
|
|
4
|
+
|
|
5
|
+
const GraphPointsCountSelector = ({
|
|
6
|
+
numPoints = 1,
|
|
7
|
+
onChange,
|
|
8
|
+
}: {
|
|
9
|
+
numPoints?: PointValue;
|
|
10
|
+
onChange: (points: PointValue) => void;
|
|
11
|
+
}) => {
|
|
12
|
+
return (
|
|
13
|
+
<select
|
|
14
|
+
value={numPoints}
|
|
15
|
+
onChange={(e) => {
|
|
16
|
+
onChange(parsePointCount(e.target.value));
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
{[...Array(7).keys()].map((n) => (
|
|
20
|
+
<option key={n} value={n}>
|
|
21
|
+
{`${n} point${n > 1 ? "s" : ""}`}
|
|
22
|
+
</option>
|
|
23
|
+
))}
|
|
24
|
+
<option value={UNLIMITED}>unlimited</option>
|
|
25
|
+
</select>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default GraphPointsCountSelector;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
type GraphTypeSelectorProps = {
|
|
4
|
+
graphType: string;
|
|
5
|
+
onChange: (newGraphType: string) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const GraphTypeSelector = (props: GraphTypeSelectorProps) => {
|
|
9
|
+
return (
|
|
10
|
+
<select
|
|
11
|
+
value={props.graphType}
|
|
12
|
+
onChange={(e) => {
|
|
13
|
+
const type = e.target.value;
|
|
14
|
+
props.onChange(type);
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<option value="linear">Linear function</option>
|
|
18
|
+
<option value="quadratic">Quadratic function</option>
|
|
19
|
+
<option value="sinusoid">Sinusoid function</option>
|
|
20
|
+
<option value="circle">Circle</option>
|
|
21
|
+
<option value="point">Point(s)</option>
|
|
22
|
+
<option value="linear-system">Linear System</option>
|
|
23
|
+
<option value="polygon">Polygon</option>
|
|
24
|
+
<option value="segment">Line Segment(s)</option>
|
|
25
|
+
<option value="ray">Ray</option>
|
|
26
|
+
<option value="angle">Angle</option>
|
|
27
|
+
</select>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default GraphTypeSelector;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import _ from "underscore";
|
|
3
|
+
|
|
4
|
+
const SegmentCountSelector = ({
|
|
5
|
+
numSegments = 1,
|
|
6
|
+
onChange,
|
|
7
|
+
}: {
|
|
8
|
+
numSegments?: number;
|
|
9
|
+
onChange: (numSegments: number) => void;
|
|
10
|
+
}) => (
|
|
11
|
+
<select
|
|
12
|
+
key="segment-select"
|
|
13
|
+
value={numSegments}
|
|
14
|
+
onChange={(e) => {
|
|
15
|
+
const num = +e.target.value;
|
|
16
|
+
onChange(num);
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
{_.range(1, 7).map((n) => (
|
|
20
|
+
<option key={n} value={n}>
|
|
21
|
+
{`${n} segment${n > 1 ? "s" : ""}`}
|
|
22
|
+
</option>
|
|
23
|
+
))}
|
|
24
|
+
</select>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export default SegmentCountSelector;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
components,
|
|
3
|
+
icons,
|
|
4
|
+
Widgets,
|
|
5
|
+
WIDGET_PROP_DENYLIST,
|
|
6
|
+
} from "@khanacademy/perseus";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import _ from "underscore";
|
|
9
|
+
|
|
10
|
+
import SectionControlButton from "./section-control-button";
|
|
11
|
+
|
|
12
|
+
import type {Alignment, WidgetInfo} from "@khanacademy/perseus";
|
|
13
|
+
|
|
14
|
+
const {InlineIcon, PropCheckBox} = components;
|
|
15
|
+
const {iconChevronDown, iconChevronRight, iconTrash} = icons;
|
|
16
|
+
|
|
17
|
+
type WidgetEditorProps = {
|
|
18
|
+
// Unserialized props
|
|
19
|
+
id: string;
|
|
20
|
+
onChange: (
|
|
21
|
+
widgetInfo: WidgetInfo,
|
|
22
|
+
cb?: () => unknown,
|
|
23
|
+
silent?: boolean,
|
|
24
|
+
) => unknown;
|
|
25
|
+
onRemove: () => unknown;
|
|
26
|
+
apiOptions: any;
|
|
27
|
+
} & WidgetInfo;
|
|
28
|
+
|
|
29
|
+
type WidgetEditorState = {
|
|
30
|
+
showWidget: boolean;
|
|
31
|
+
widgetInfo: WidgetInfo;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const _upgradeWidgetInfo = (props: WidgetEditorProps): React.ReactElement => {
|
|
35
|
+
// We can't call serialize here because this.refs.widget
|
|
36
|
+
// doesn't exist before this component is mounted.
|
|
37
|
+
const filteredProps = _.omit(props, WIDGET_PROP_DENYLIST);
|
|
38
|
+
// @ts-expect-error [FEI-5003] - 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
|
+
return Widgets.upgradeWidgetInfoToLatestVersion(filteredProps);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// This component handles upgading widget editor props via prop
|
|
43
|
+
// upgrade transforms. Widget editors will always be rendered
|
|
44
|
+
// with all available transforms applied, but the results of those
|
|
45
|
+
// transforms will not be propogated upwards until serialization.
|
|
46
|
+
// eslint-disable-next-line react/no-unsafe
|
|
47
|
+
class WidgetEditor extends React.Component<
|
|
48
|
+
WidgetEditorProps,
|
|
49
|
+
WidgetEditorState
|
|
50
|
+
> {
|
|
51
|
+
constructor(props: WidgetEditorProps) {
|
|
52
|
+
super(props);
|
|
53
|
+
this.state = {
|
|
54
|
+
showWidget: false,
|
|
55
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
|
|
56
|
+
widgetInfo: _upgradeWidgetInfo(props),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
UNSAFE_componentWillReceiveProps(nextProps: WidgetEditorProps) {
|
|
61
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
|
|
62
|
+
this.setState({widgetInfo: _upgradeWidgetInfo(nextProps)});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_toggleWidget = (e: React.SyntheticEvent) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
this.setState({showWidget: !this.state.showWidget});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
_handleWidgetChange = (
|
|
71
|
+
newProps: WidgetEditorProps,
|
|
72
|
+
cb: () => unknown,
|
|
73
|
+
silent: boolean,
|
|
74
|
+
) => {
|
|
75
|
+
const newWidgetInfo = Object.assign(
|
|
76
|
+
{},
|
|
77
|
+
this.state.widgetInfo,
|
|
78
|
+
) as WidgetInfo;
|
|
79
|
+
newWidgetInfo.options = Object.assign(
|
|
80
|
+
// eslint-disable-next-line react/no-string-refs
|
|
81
|
+
// @ts-expect-error [FEI-5003] - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
|
|
82
|
+
this.refs.widget.serialize(),
|
|
83
|
+
newProps,
|
|
84
|
+
);
|
|
85
|
+
this.props.onChange(newWidgetInfo, cb, silent);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
_toggleStatic = (e: Event) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
const newWidgetInfo = Object.assign({}, this.state.widgetInfo, {
|
|
91
|
+
static: !this.state.widgetInfo.static,
|
|
92
|
+
}) as WidgetInfo;
|
|
93
|
+
this.props.onChange(newWidgetInfo);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
_handleAlignmentChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
|
97
|
+
const newAlignment = e.currentTarget.value as Alignment;
|
|
98
|
+
const newWidgetInfo = Object.assign(
|
|
99
|
+
{},
|
|
100
|
+
this.state.widgetInfo,
|
|
101
|
+
) as WidgetInfo;
|
|
102
|
+
newWidgetInfo.alignment = newAlignment;
|
|
103
|
+
this.props.onChange(newWidgetInfo);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
getSaveWarnings = () => {
|
|
107
|
+
// eslint-disable-next-line react/no-string-refs
|
|
108
|
+
// @ts-expect-error [FEI-5003] - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
|
|
109
|
+
const issuesFunc = this.refs.widget.getSaveWarnings;
|
|
110
|
+
return issuesFunc ? issuesFunc() : [];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
serialize = () => {
|
|
114
|
+
// TODO(alex): Make this properly handle the case where we load json
|
|
115
|
+
// with a more recent widget version than this instance of Perseus
|
|
116
|
+
// knows how to handle.
|
|
117
|
+
const widgetInfo = this.state.widgetInfo;
|
|
118
|
+
return {
|
|
119
|
+
type: widgetInfo.type,
|
|
120
|
+
alignment: widgetInfo.alignment,
|
|
121
|
+
static: widgetInfo.static,
|
|
122
|
+
graded: widgetInfo.graded,
|
|
123
|
+
// eslint-disable-next-line react/no-string-refs
|
|
124
|
+
// @ts-expect-error [FEI-5003] - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
|
|
125
|
+
options: this.refs.widget.serialize(),
|
|
126
|
+
version: widgetInfo.version,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
render(): React.ReactNode {
|
|
131
|
+
const widgetInfo = this.state.widgetInfo;
|
|
132
|
+
|
|
133
|
+
const Ed = Widgets.getEditor(widgetInfo.type);
|
|
134
|
+
let supportedAlignments: ReadonlyArray<Alignment>;
|
|
135
|
+
if (this.props.apiOptions.showAlignmentOptions) {
|
|
136
|
+
supportedAlignments = Widgets.getSupportedAlignments(
|
|
137
|
+
widgetInfo.type,
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
// NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js.
|
|
141
|
+
supportedAlignments = ["default"];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type);
|
|
145
|
+
|
|
146
|
+
const isUngradedEnabled = widgetInfo.type === "transformer";
|
|
147
|
+
const gradedPropBox = (
|
|
148
|
+
<PropCheckBox
|
|
149
|
+
label="Graded:"
|
|
150
|
+
graded={widgetInfo.graded}
|
|
151
|
+
onChange={this.props.onChange}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className="perseus-widget-editor">
|
|
157
|
+
<div
|
|
158
|
+
className={
|
|
159
|
+
"perseus-widget-editor-title " +
|
|
160
|
+
(this.state.showWidget ? "open" : "closed")
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
164
|
+
<a
|
|
165
|
+
className="perseus-widget-editor-title-id"
|
|
166
|
+
href="#"
|
|
167
|
+
onClick={this._toggleWidget}
|
|
168
|
+
>
|
|
169
|
+
{this.props.id}
|
|
170
|
+
{this.state.showWidget ? (
|
|
171
|
+
<InlineIcon {...iconChevronDown} />
|
|
172
|
+
) : (
|
|
173
|
+
<InlineIcon {...iconChevronRight} />
|
|
174
|
+
)}
|
|
175
|
+
</a>
|
|
176
|
+
|
|
177
|
+
{supportsStaticMode && (
|
|
178
|
+
<input
|
|
179
|
+
type="button"
|
|
180
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type '(e: Event) => void' is not assignable to type 'MouseEventHandler<HTMLInputElement>'.
|
|
181
|
+
onClick={this._toggleStatic}
|
|
182
|
+
className="simple-button--small"
|
|
183
|
+
value={
|
|
184
|
+
widgetInfo.static
|
|
185
|
+
? "Unset as static"
|
|
186
|
+
: "Set as static"
|
|
187
|
+
}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
{supportedAlignments.length > 1 && (
|
|
191
|
+
<select
|
|
192
|
+
className="alignment"
|
|
193
|
+
value={widgetInfo.alignment}
|
|
194
|
+
onChange={this._handleAlignmentChange}
|
|
195
|
+
>
|
|
196
|
+
{supportedAlignments.map((alignment) => (
|
|
197
|
+
<option key={alignment}>{alignment}</option>
|
|
198
|
+
))}
|
|
199
|
+
</select>
|
|
200
|
+
)}
|
|
201
|
+
<SectionControlButton
|
|
202
|
+
icon={iconTrash}
|
|
203
|
+
onClick={() => {
|
|
204
|
+
this.props.onRemove();
|
|
205
|
+
}}
|
|
206
|
+
title="Remove image widget"
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
<div
|
|
210
|
+
className={
|
|
211
|
+
"perseus-widget-editor-content " +
|
|
212
|
+
(this.state.showWidget ? "enter" : "leave")
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
{isUngradedEnabled && gradedPropBox}
|
|
216
|
+
{Ed && (
|
|
217
|
+
<Ed
|
|
218
|
+
// eslint-disable-next-line react/no-string-refs
|
|
219
|
+
ref="widget"
|
|
220
|
+
onChange={this._handleWidgetChange}
|
|
221
|
+
static={widgetInfo.static}
|
|
222
|
+
apiOptions={this.props.apiOptions}
|
|
223
|
+
{...widgetInfo.options}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default WidgetEditor;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {Widgets} from "@khanacademy/perseus";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import _ from "underscore";
|
|
4
|
+
|
|
5
|
+
type WidgetSelectProps = {
|
|
6
|
+
onChange?: (widgetType: string) => unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
class WidgetSelect extends React.Component<WidgetSelectProps> {
|
|
10
|
+
shouldComponentUpdate() {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
handleChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
|
15
|
+
const widgetType = e.currentTarget.value;
|
|
16
|
+
if (widgetType === "") {
|
|
17
|
+
// TODO(alpert): Not sure if change will trigger here
|
|
18
|
+
// but might as well be safe
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (this.props.onChange) {
|
|
22
|
+
this.props.onChange(widgetType);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
render(): React.ReactNode {
|
|
27
|
+
const widgets = Widgets.getPublicWidgets();
|
|
28
|
+
const orderedWidgetNames = _.sortBy(_.keys(widgets), (name) => {
|
|
29
|
+
return widgets[name].displayName;
|
|
30
|
+
});
|
|
31
|
+
const addWidgetString = "Add a widget\u2026";
|
|
32
|
+
return (
|
|
33
|
+
<select value="" onChange={this.handleChange}>
|
|
34
|
+
<option value="">{addWidgetString}</option>
|
|
35
|
+
<option disabled>--</option>
|
|
36
|
+
{_.map(orderedWidgetNames, (name) => {
|
|
37
|
+
return (
|
|
38
|
+
<option key={name} value={name}>
|
|
39
|
+
{widgets[name].displayName}
|
|
40
|
+
</option>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</select>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default WidgetSelect;
|