@khanacademy/perseus-editor 3.0.1 → 4.1.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,72 @@
1
+ import {ApiOptions} from "@khanacademy/perseus";
2
+ import {View} from "@khanacademy/wonder-blocks-core";
3
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
4
+ import {LabelSmall} from "@khanacademy/wonder-blocks-typography";
5
+ import {action} from "@storybook/addon-actions";
6
+ import {StyleSheet} from "aphrodite";
7
+ import * as React from "react";
8
+
9
+ import ImageEditor from "../image-editor";
10
+
11
+ import type {PropsFor} from "@khanacademy/wonder-blocks-core";
12
+
13
+ type StoryArgs = Record<any, any>;
14
+
15
+ type Story = {
16
+ title: string;
17
+ };
18
+
19
+ export default {
20
+ title: "Perseus/Editor/Widgets/Image Editor",
21
+ } as Story;
22
+
23
+ const styles = StyleSheet.create({
24
+ wrapper: {
25
+ // The maximum width of a widget in the editor.
26
+ width: 338,
27
+ margin: 20,
28
+ },
29
+ });
30
+
31
+ const onChangeAction = action("onChange");
32
+
33
+ const WithState = () => {
34
+ const widgetRef = React.useRef<ImageEditor>(null);
35
+ const [state, setState] = React.useState<
36
+ Partial<PropsFor<typeof ImageEditor>>
37
+ >({
38
+ backgroundImage: {
39
+ url: "https://ka-perseus-images.s3.amazonaws.com/2ee5fc32e35c5178373b39fd304b325b2994c913.png",
40
+ },
41
+ });
42
+
43
+ return (
44
+ <View style={styles.wrapper}>
45
+ <LabelSmall
46
+ style={{fontStyle: "italic", marginBottom: Spacing.small_12}}
47
+ >
48
+ <b>Note</b> that this editor has a known-issue where it does not
49
+ calculate the image dimensions initially if they aren't
50
+ provided. It does update the dimensions when you blur the 'Image
51
+ url:' field.
52
+ </LabelSmall>
53
+ <ImageEditor
54
+ {...state}
55
+ apiOptions={ApiOptions.defaults}
56
+ onChange={(props) => {
57
+ onChangeAction(props);
58
+
59
+ setState({
60
+ ...widgetRef.current?.serialize(),
61
+ ...props,
62
+ });
63
+ }}
64
+ ref={widgetRef}
65
+ />
66
+ </View>
67
+ );
68
+ };
69
+
70
+ export const Default = (args: StoryArgs): React.ReactElement => {
71
+ return <WithState />;
72
+ };
@@ -6,17 +6,18 @@ import {
6
6
  EditorJsonify,
7
7
  Util,
8
8
  } from "@khanacademy/perseus";
9
- import createReactClass from "create-react-class";
10
9
  import * as React from "react";
11
10
  import _ from "underscore";
12
11
 
13
12
  import BlurInput from "../components/blur-input";
14
13
  import Editor from "../editor";
15
14
 
15
+ import type {APIOptions, Range, Size} from "@khanacademy/perseus";
16
+
16
17
  const {InfoTip, InlineIcon, RangeInput} = components;
17
18
 
18
19
  const defaultBoxSize = 400;
19
- const defaultRange = [0, 10];
20
+ const defaultRange = [0, 10] as const;
20
21
  const defaultBackgroundImage = {
21
22
  url: null,
22
23
  width: 0,
@@ -53,46 +54,63 @@ const captionAlignments = [
53
54
  "above left",
54
55
  ];
55
56
 
56
- const ImageEditor: any = createReactClass({
57
- displayName: "ImageEditor",
58
-
59
- propTypes: {
60
- ...Changeable.propTypes,
61
- },
62
-
63
- statics: {
64
- widgetName: "image",
65
- },
66
-
67
- componentDidMount: function () {
57
+ type Props = Changeable.ChangeableProps & {
58
+ apiOptions: APIOptions;
59
+
60
+ title: string;
61
+ range: [Readonly<Range>, Readonly<Range>];
62
+ box: Size;
63
+ backgroundImage: any;
64
+ labels: ReadonlyArray<string>;
65
+ alt: string;
66
+ caption: string;
67
+ };
68
+
69
+ type DefaultProps = {
70
+ title: NonNullable<Props["title"]>;
71
+ range: NonNullable<Props["range"]>;
72
+ box: NonNullable<Props["box"]>;
73
+ backgroundImage: NonNullable<Props["backgroundImage"]>;
74
+ labels: NonNullable<Props["labels"]>;
75
+ alt: NonNullable<Props["alt"]>;
76
+ caption: NonNullable<Props["caption"]>;
77
+ };
78
+
79
+ type State = {
80
+ backgroundImageError?: string;
81
+ };
82
+
83
+ class ImageEditor extends React.Component<Props> {
84
+ _isMounted = false;
85
+
86
+ static displayName = "ImageEditor";
87
+ static widgetName = "image";
88
+
89
+ static defaultProps: DefaultProps = {
90
+ title: "",
91
+ range: [defaultRange, defaultRange],
92
+ box: [defaultBoxSize, defaultBoxSize],
93
+ backgroundImage: defaultBackgroundImage,
94
+ labels: [],
95
+ alt: "",
96
+ caption: "",
97
+ };
98
+
99
+ state: State = {
100
+ backgroundImageError: "",
101
+ };
102
+
103
+ componentDidMount() {
68
104
  // TODO(scottgrant): This is a hack to remove the deprecated call to
69
105
  // this.isMounted() but is still considered an anti-pattern.
70
106
  this._isMounted = true;
71
- },
107
+ }
72
108
 
73
- componentWillUnmount: function () {
109
+ componentWillUnmount() {
74
110
  this._isMounted = false;
75
- },
76
-
77
- getDefaultProps: function () {
78
- return {
79
- title: "",
80
- range: [defaultRange, defaultRange],
81
- box: [defaultBoxSize, defaultBoxSize],
82
- backgroundImage: defaultBackgroundImage,
83
- labels: [],
84
- alt: "",
85
- caption: "",
86
- };
87
- },
88
-
89
- getInitialState: function () {
90
- return {
91
- backgroundImageError: "",
92
- };
93
- },
94
-
95
- render: function () {
111
+ }
112
+
113
+ render() {
96
114
  const backgroundImage = this.props.backgroundImage;
97
115
 
98
116
  const imageSettings = (
@@ -183,9 +201,9 @@ const ImageEditor: any = createReactClass({
183
201
  {backgroundImage.url && imageSettings}
184
202
  </div>
185
203
  );
186
- },
204
+ }
187
205
 
188
- _renderRowForLabel: function (label, i) {
206
+ _renderRowForLabel(label, i) {
189
207
  return (
190
208
  <tr key={i}>
191
209
  <td>
@@ -233,46 +251,46 @@ const ImageEditor: any = createReactClass({
233
251
  </td>
234
252
  </tr>
235
253
  );
236
- },
254
+ }
237
255
 
238
256
  change(...args) {
239
257
  return Changeable.change.apply(this, args);
240
- },
258
+ }
241
259
 
242
- removeLabel: function (labelIndex, e) {
260
+ removeLabel(labelIndex, e) {
243
261
  e.preventDefault();
244
- const labels = _(this.props.labels).clone();
262
+ const labels = [...this.props.labels];
245
263
  labels.splice(labelIndex, 1);
246
264
  this.props.onChange({labels: labels});
247
- },
265
+ }
248
266
 
249
- onCoordinateChange: function (labelIndex, newCoordinates) {
267
+ onCoordinateChange(labelIndex, newCoordinates) {
250
268
  const labels = this.props.labels.slice();
251
269
  labels[labelIndex] = _.extend({}, labels[labelIndex], {
252
270
  coordinates: newCoordinates,
253
271
  });
254
272
  this.props.onChange({labels: labels});
255
- },
273
+ }
256
274
 
257
- onContentChange: function (labelIndex, e) {
275
+ onContentChange(labelIndex, e) {
258
276
  const newContent = e.target.value;
259
277
  const labels = this.props.labels.slice();
260
278
  labels[labelIndex] = _.extend({}, labels[labelIndex], {
261
279
  content: newContent,
262
280
  });
263
281
  this.props.onChange({labels: labels});
264
- },
282
+ }
265
283
 
266
- onAlignmentChange: function (labelIndex, e) {
284
+ onAlignmentChange(labelIndex, e) {
267
285
  const newAlignment = e.target.value;
268
286
  const labels = this.props.labels.slice();
269
287
  labels[labelIndex] = _.extend({}, labels[labelIndex], {
270
288
  alignment: newAlignment,
271
289
  });
272
290
  this.props.onChange({labels: labels});
273
- },
291
+ }
274
292
 
275
- setUrl: function (url, width, height, silent) {
293
+ setUrl(url, width, height, silent) {
276
294
  // Because this calls into WidgetEditor._handleWidgetChange, which
277
295
  // checks for this widget's ref to serialize it.
278
296
  //
@@ -295,15 +313,12 @@ const ImageEditor: any = createReactClass({
295
313
  null,
296
314
  silent,
297
315
  );
298
- },
316
+ }
299
317
 
300
318
  // silently load the image when the component mounts
301
319
  // silently update url and sizes when the image loads
302
320
  // noisily load the image in response to the author changing it
303
- onUrlChange: async function (
304
- url: string | undefined | null,
305
- silent: boolean,
306
- ) {
321
+ async onUrlChange(url: string | undefined | null, silent: boolean) {
307
322
  // Check if we've been passed something that looks like a URL
308
323
  if (!url) {
309
324
  this.setUrl(url, 0, 0, silent);
@@ -336,17 +351,17 @@ const ImageEditor: any = createReactClass({
336
351
  )}`,
337
352
  });
338
353
  }
339
- },
354
+ }
340
355
 
341
- onRangeChange: function (type, newRange) {
356
+ onRangeChange(type, newRange) {
342
357
  const range = this.props.range.slice();
343
358
  range[type] = newRange;
344
359
  this.props.onChange({range: range});
345
- },
360
+ }
346
361
 
347
362
  serialize() {
348
363
  return EditorJsonify.serialize.call(this);
349
- },
350
- });
364
+ }
365
+ }
351
366
 
352
367
  export default ImageEditor;
@@ -46,7 +46,7 @@ export default function MathquillInput(props: Props) {
46
46
  props.onChange(value);
47
47
  }
48
48
  },
49
- upOutOf: (mathField) => {
49
+ upOutOf: (mathField: MathFieldInterface) => {
50
50
  // This handler is called when the user presses the up
51
51
  // arrow key, but there is nowhere in the expression to go
52
52
  // up to (no numerator or exponent). For ease of use,