@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/all-widgets.d.ts +2 -2
  3. package/dist/all-widgets.js.flow +2 -2
  4. package/dist/components/graph-points-count-selector.d.ts +7 -0
  5. package/dist/components/graph-points-count-selector.js.flow +12 -0
  6. package/dist/components/graph-type-selector.d.ts +7 -0
  7. package/dist/components/graph-type-selector.js.flow +12 -0
  8. package/dist/components/segment-count-selector.d.ts +6 -0
  9. package/dist/components/segment-count-selector.js.flow +11 -0
  10. package/dist/components/widget-editor.d.ts +31 -0
  11. package/dist/components/widget-editor.js.flow +91 -0
  12. package/dist/components/widget-select.d.ts +10 -0
  13. package/dist/components/widget-select.js.flow +16 -0
  14. package/dist/es/index.js +423 -229
  15. package/dist/es/index.js.map +1 -1
  16. package/dist/index.js +431 -225
  17. package/dist/index.js.map +1 -1
  18. package/dist/testing-widgets.d.ts +2 -2
  19. package/dist/testing-widgets.js.flow +2 -2
  20. package/dist/util/points.d.ts +3 -0
  21. package/dist/util/points.js.flow +9 -0
  22. package/dist/widgets/example-graphie-widget.d.ts +2 -2
  23. package/dist/widgets/example-graphie-widget.js.flow +2 -2
  24. package/dist/widgets/example-widget.d.ts +2 -2
  25. package/dist/widgets/example-widget.js.flow +2 -2
  26. package/dist/widgets/interactive-graph-editor.d.ts +3182 -1
  27. package/dist/widgets/interactive-graph-editor.js.flow +5290 -1
  28. package/package.json +9 -3
  29. package/src/__stories__/editor.stories.tsx +105 -4
  30. package/src/article-editor.tsx +1 -1
  31. package/src/{__stories__ → components/__stories__}/section-control-button.stories.tsx +1 -1
  32. package/src/components/graph-points-count-selector.tsx +29 -0
  33. package/src/components/graph-type-selector.tsx +31 -0
  34. package/src/components/json-editor.tsx +0 -1
  35. package/src/components/segment-count-selector.tsx +27 -0
  36. package/src/components/widget-editor.tsx +232 -0
  37. package/src/components/widget-select.tsx +48 -0
  38. package/src/editor.tsx +4 -270
  39. package/src/util/points.test.ts +24 -0
  40. package/src/util/points.ts +13 -0
  41. package/src/widgets/interactive-graph-editor.tsx +294 -48
  42. package/tsconfig-build.json +0 -1
  43. package/tsconfig-build.tsbuildinfo +1 -1
  44. package/dist/diffs/__stories__/perseus-diff-wrapper.d.ts +0 -6
  45. package/dist/diffs/__stories__/perseus-diff-wrapper.js.flow +0 -12
  46. /package/dist/{section-control-button.d.ts → components/section-control-button.d.ts} +0 -0
  47. /package/dist/{section-control-button.js.flow → components/section-control-button.js.flow} +0 -0
  48. /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 {Alignment, ChangeHandler, WidgetInfo} from "@khanacademy/perseus";
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
+ };