@khanacademy/perseus-editor 2.2.2 → 2.3.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 +15 -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/src/editor.tsx
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
components,
|
|
3
|
-
icons,
|
|
4
2
|
preprocessTex,
|
|
5
3
|
Errors,
|
|
6
4
|
Log,
|
|
@@ -8,7 +6,6 @@ import {
|
|
|
8
6
|
PerseusMarkdown,
|
|
9
7
|
Util,
|
|
10
8
|
Widgets,
|
|
11
|
-
WIDGET_PROP_DENYLIST,
|
|
12
9
|
} from "@khanacademy/perseus";
|
|
13
10
|
import $ from "jquery";
|
|
14
11
|
// eslint-disable-next-line import/no-unresolved, import/no-extraneous-dependencies
|
|
@@ -18,13 +15,11 @@ import * as ReactDOM from "react-dom";
|
|
|
18
15
|
import _ from "underscore";
|
|
19
16
|
|
|
20
17
|
import DragTarget from "./components/drag-target";
|
|
18
|
+
import WidgetEditor from "./components/widget-editor";
|
|
19
|
+
import WidgetSelect from "./components/widget-select";
|
|
21
20
|
import KatexErrorView from "./katex-error-view";
|
|
22
|
-
import SectionControlButton from "./section-control-button";
|
|
23
21
|
|
|
24
|
-
import type {
|
|
25
|
-
|
|
26
|
-
const {InlineIcon, PropCheckBox} = components;
|
|
27
|
-
const {iconChevronDown, iconChevronRight, iconTrash} = icons;
|
|
22
|
+
import type {ChangeHandler, WidgetInfo} from "@khanacademy/perseus";
|
|
28
23
|
|
|
29
24
|
// like [[snowman input-number 1]]
|
|
30
25
|
const widgetPlaceholder = "[[\u2603 {id}]]";
|
|
@@ -66,264 +61,6 @@ const makeStartWithAParagraphAlways = (content) => {
|
|
|
66
61
|
return "\n\n".slice(0, 2 - newlines.length) + content;
|
|
67
62
|
};
|
|
68
63
|
|
|
69
|
-
type WidgetSelectProps = {
|
|
70
|
-
onChange?: (widgetType: string) => unknown;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
class WidgetSelect extends React.Component<WidgetSelectProps> {
|
|
74
|
-
shouldComponentUpdate() {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
handleChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
|
79
|
-
const widgetType = e.currentTarget.value;
|
|
80
|
-
if (widgetType === "") {
|
|
81
|
-
// TODO(alpert): Not sure if change will trigger here
|
|
82
|
-
// but might as well be safe
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (this.props.onChange) {
|
|
86
|
-
this.props.onChange(widgetType);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
render(): React.ReactNode {
|
|
91
|
-
const widgets = Widgets.getPublicWidgets();
|
|
92
|
-
const orderedWidgetNames = _.sortBy(_.keys(widgets), (name) => {
|
|
93
|
-
return widgets[name].displayName;
|
|
94
|
-
});
|
|
95
|
-
const addWidgetString = "Add a widget\u2026";
|
|
96
|
-
return (
|
|
97
|
-
<select value="" onChange={this.handleChange}>
|
|
98
|
-
<option value="">{addWidgetString}</option>
|
|
99
|
-
<option disabled>--</option>
|
|
100
|
-
{_.map(orderedWidgetNames, (name) => {
|
|
101
|
-
return (
|
|
102
|
-
<option key={name} value={name}>
|
|
103
|
-
{widgets[name].displayName}
|
|
104
|
-
</option>
|
|
105
|
-
);
|
|
106
|
-
})}
|
|
107
|
-
</select>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
type WidgetEditorProps = {
|
|
113
|
-
// Unserialized props
|
|
114
|
-
id: string;
|
|
115
|
-
onChange: (
|
|
116
|
-
widgetInfo: WidgetInfo,
|
|
117
|
-
cb?: () => unknown,
|
|
118
|
-
silent?: boolean,
|
|
119
|
-
) => unknown;
|
|
120
|
-
onRemove: () => unknown;
|
|
121
|
-
apiOptions: any;
|
|
122
|
-
} & WidgetInfo;
|
|
123
|
-
|
|
124
|
-
type WidgetEditorState = {
|
|
125
|
-
showWidget: boolean;
|
|
126
|
-
widgetInfo: WidgetInfo;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const _upgradeWidgetInfo = (props: WidgetEditorProps): React.ReactElement => {
|
|
130
|
-
// We can't call serialize here because this.refs.widget
|
|
131
|
-
// doesn't exist before this component is mounted.
|
|
132
|
-
const filteredProps = _.omit(props, WIDGET_PROP_DENYLIST);
|
|
133
|
-
// @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'.
|
|
134
|
-
return Widgets.upgradeWidgetInfoToLatestVersion(filteredProps);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// This component handles upgading widget editor props via prop
|
|
138
|
-
// upgrade transforms. Widget editors will always be rendered
|
|
139
|
-
// with all available transforms applied, but the results of those
|
|
140
|
-
// transforms will not be propogated upwards until serialization.
|
|
141
|
-
// eslint-disable-next-line react/no-unsafe
|
|
142
|
-
class WidgetEditor extends React.Component<
|
|
143
|
-
WidgetEditorProps,
|
|
144
|
-
WidgetEditorState
|
|
145
|
-
> {
|
|
146
|
-
constructor(props: WidgetEditorProps) {
|
|
147
|
-
super(props);
|
|
148
|
-
this.state = {
|
|
149
|
-
showWidget: false,
|
|
150
|
-
// @ts-expect-error [FEI-5003] - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
|
|
151
|
-
widgetInfo: _upgradeWidgetInfo(props),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
UNSAFE_componentWillReceiveProps(nextProps: WidgetEditorProps) {
|
|
156
|
-
// @ts-expect-error [FEI-5003] - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
|
|
157
|
-
this.setState({widgetInfo: _upgradeWidgetInfo(nextProps)});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
_toggleWidget = (e: React.SyntheticEvent) => {
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
this.setState({showWidget: !this.state.showWidget});
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
_handleWidgetChange = (
|
|
166
|
-
newProps: WidgetEditorProps,
|
|
167
|
-
cb: () => unknown,
|
|
168
|
-
silent: boolean,
|
|
169
|
-
) => {
|
|
170
|
-
const newWidgetInfo = Object.assign(
|
|
171
|
-
{},
|
|
172
|
-
this.state.widgetInfo,
|
|
173
|
-
) as WidgetInfo;
|
|
174
|
-
newWidgetInfo.options = Object.assign(
|
|
175
|
-
// eslint-disable-next-line react/no-string-refs
|
|
176
|
-
// @ts-expect-error [FEI-5003] - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
|
|
177
|
-
this.refs.widget.serialize(),
|
|
178
|
-
newProps,
|
|
179
|
-
);
|
|
180
|
-
this.props.onChange(newWidgetInfo, cb, silent);
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
_toggleStatic = (e: Event) => {
|
|
184
|
-
e.preventDefault();
|
|
185
|
-
const newWidgetInfo = Object.assign({}, this.state.widgetInfo, {
|
|
186
|
-
static: !this.state.widgetInfo.static,
|
|
187
|
-
}) as WidgetInfo;
|
|
188
|
-
this.props.onChange(newWidgetInfo);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
_handleAlignmentChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
|
192
|
-
const newAlignment = e.currentTarget.value as Alignment;
|
|
193
|
-
const newWidgetInfo = Object.assign(
|
|
194
|
-
{},
|
|
195
|
-
this.state.widgetInfo,
|
|
196
|
-
) as WidgetInfo;
|
|
197
|
-
newWidgetInfo.alignment = newAlignment;
|
|
198
|
-
this.props.onChange(newWidgetInfo);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
getSaveWarnings = () => {
|
|
202
|
-
// eslint-disable-next-line react/no-string-refs
|
|
203
|
-
// @ts-expect-error [FEI-5003] - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
|
|
204
|
-
const issuesFunc = this.refs.widget.getSaveWarnings;
|
|
205
|
-
return issuesFunc ? issuesFunc() : [];
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
serialize = () => {
|
|
209
|
-
// TODO(alex): Make this properly handle the case where we load json
|
|
210
|
-
// with a more recent widget version than this instance of Perseus
|
|
211
|
-
// knows how to handle.
|
|
212
|
-
const widgetInfo = this.state.widgetInfo;
|
|
213
|
-
return {
|
|
214
|
-
type: widgetInfo.type,
|
|
215
|
-
alignment: widgetInfo.alignment,
|
|
216
|
-
static: widgetInfo.static,
|
|
217
|
-
graded: widgetInfo.graded,
|
|
218
|
-
// eslint-disable-next-line react/no-string-refs
|
|
219
|
-
// @ts-expect-error [FEI-5003] - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
|
|
220
|
-
options: this.refs.widget.serialize(),
|
|
221
|
-
version: widgetInfo.version,
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
render(): React.ReactNode {
|
|
226
|
-
const widgetInfo = this.state.widgetInfo;
|
|
227
|
-
|
|
228
|
-
const Ed = Widgets.getEditor(widgetInfo.type);
|
|
229
|
-
let supportedAlignments: ReadonlyArray<Alignment>;
|
|
230
|
-
if (this.props.apiOptions.showAlignmentOptions) {
|
|
231
|
-
supportedAlignments = Widgets.getSupportedAlignments(
|
|
232
|
-
widgetInfo.type,
|
|
233
|
-
);
|
|
234
|
-
} else {
|
|
235
|
-
// NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js.
|
|
236
|
-
supportedAlignments = ["default"];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type);
|
|
240
|
-
|
|
241
|
-
const isUngradedEnabled = widgetInfo.type === "transformer";
|
|
242
|
-
const gradedPropBox = (
|
|
243
|
-
<PropCheckBox
|
|
244
|
-
label="Graded:"
|
|
245
|
-
graded={widgetInfo.graded}
|
|
246
|
-
onChange={this.props.onChange}
|
|
247
|
-
/>
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
return (
|
|
251
|
-
<div className="perseus-widget-editor">
|
|
252
|
-
<div
|
|
253
|
-
className={
|
|
254
|
-
"perseus-widget-editor-title " +
|
|
255
|
-
(this.state.showWidget ? "open" : "closed")
|
|
256
|
-
}
|
|
257
|
-
>
|
|
258
|
-
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
259
|
-
<a
|
|
260
|
-
className="perseus-widget-editor-title-id"
|
|
261
|
-
href="#"
|
|
262
|
-
onClick={this._toggleWidget}
|
|
263
|
-
>
|
|
264
|
-
{this.props.id}
|
|
265
|
-
{this.state.showWidget ? (
|
|
266
|
-
<InlineIcon {...iconChevronDown} />
|
|
267
|
-
) : (
|
|
268
|
-
<InlineIcon {...iconChevronRight} />
|
|
269
|
-
)}
|
|
270
|
-
</a>
|
|
271
|
-
|
|
272
|
-
{supportsStaticMode && (
|
|
273
|
-
<input
|
|
274
|
-
type="button"
|
|
275
|
-
// @ts-expect-error [FEI-5003] - TS2322 - Type '(e: Event) => void' is not assignable to type 'MouseEventHandler<HTMLInputElement>'.
|
|
276
|
-
onClick={this._toggleStatic}
|
|
277
|
-
className="simple-button--small"
|
|
278
|
-
value={
|
|
279
|
-
widgetInfo.static
|
|
280
|
-
? "Unset as static"
|
|
281
|
-
: "Set as static"
|
|
282
|
-
}
|
|
283
|
-
/>
|
|
284
|
-
)}
|
|
285
|
-
{supportedAlignments.length > 1 && (
|
|
286
|
-
<select
|
|
287
|
-
className="alignment"
|
|
288
|
-
value={widgetInfo.alignment}
|
|
289
|
-
onChange={this._handleAlignmentChange}
|
|
290
|
-
>
|
|
291
|
-
{supportedAlignments.map((alignment) => (
|
|
292
|
-
<option key={alignment}>{alignment}</option>
|
|
293
|
-
))}
|
|
294
|
-
</select>
|
|
295
|
-
)}
|
|
296
|
-
<SectionControlButton
|
|
297
|
-
icon={iconTrash}
|
|
298
|
-
onClick={() => {
|
|
299
|
-
this.props.onRemove();
|
|
300
|
-
}}
|
|
301
|
-
title="Remove image widget"
|
|
302
|
-
/>
|
|
303
|
-
</div>
|
|
304
|
-
<div
|
|
305
|
-
className={
|
|
306
|
-
"perseus-widget-editor-content " +
|
|
307
|
-
(this.state.showWidget ? "enter" : "leave")
|
|
308
|
-
}
|
|
309
|
-
>
|
|
310
|
-
{isUngradedEnabled && gradedPropBox}
|
|
311
|
-
{Ed && (
|
|
312
|
-
<Ed
|
|
313
|
-
// eslint-disable-next-line react/no-string-refs
|
|
314
|
-
ref="widget"
|
|
315
|
-
onChange={this._handleWidgetChange}
|
|
316
|
-
static={widgetInfo.static}
|
|
317
|
-
apiOptions={this.props.apiOptions}
|
|
318
|
-
{...widgetInfo.options}
|
|
319
|
-
/>
|
|
320
|
-
)}
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
64
|
// This is more general than the actual markdown image parsing regex,
|
|
328
65
|
// which is fine for correctness since it should only mean we could
|
|
329
66
|
// store extra image dimensions, unless the question is insanely
|
|
@@ -1254,10 +991,7 @@ class Editor extends React.Component<Props, State> {
|
|
|
1254
991
|
// }, this);
|
|
1255
992
|
|
|
1256
993
|
this.widgetIds = _.keys(widgets);
|
|
1257
|
-
widgetsDropDown =
|
|
1258
|
-
// eslint-disable-next-line react/no-string-refs
|
|
1259
|
-
<WidgetSelect ref="widgetSelect" onChange={this._addWidget} />
|
|
1260
|
-
);
|
|
994
|
+
widgetsDropDown = <WidgetSelect onChange={this._addWidget} />;
|
|
1261
995
|
|
|
1262
996
|
const insertTemplateString = "Insert template\u2026";
|
|
1263
997
|
templatesDropDown = (
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {parsePointCount} from "./points";
|
|
2
|
+
|
|
3
|
+
describe("parsePointCount", () => {
|
|
4
|
+
it.each([
|
|
5
|
+
// Special case, 0 means unlimited points
|
|
6
|
+
["0", "unlimited"],
|
|
7
|
+
// Regular cases, ints as a string
|
|
8
|
+
["1", 1],
|
|
9
|
+
["2", 2],
|
|
10
|
+
["3", 3],
|
|
11
|
+
["4", 4],
|
|
12
|
+
["5", 5],
|
|
13
|
+
["6", 6],
|
|
14
|
+
["7", 7],
|
|
15
|
+
["8", 8],
|
|
16
|
+
["9", 9],
|
|
17
|
+
["10", 10],
|
|
18
|
+
// Parse-error cases - all map to "unlimited"
|
|
19
|
+
["non-numeric string", "unlimited"],
|
|
20
|
+
["one", "unlimited"],
|
|
21
|
+
])("Should parse %s to %s", (input, expected) => {
|
|
22
|
+
expect(parsePointCount(input)).toEqual(expected);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const UNLIMITED = "unlimited" as const;
|
|
2
|
+
export type PointValue = number | typeof UNLIMITED;
|
|
3
|
+
|
|
4
|
+
// Parses a string (ostensibly from some UI element that only supports string
|
|
5
|
+
// types, such as a <select />) into a numeric point count or the special sigil
|
|
6
|
+
// "unlimited". Note that 0 is a special case, and means "unlimited".
|
|
7
|
+
export const parsePointCount = (points: string): PointValue => {
|
|
8
|
+
const parsed = parseInt(points, 10);
|
|
9
|
+
if (isNaN(parsed)) {
|
|
10
|
+
return UNLIMITED;
|
|
11
|
+
}
|
|
12
|
+
return parsed === 0 ? UNLIMITED : parsed;
|
|
13
|
+
};
|