@khanacademy/perseus-editor 4.1.4 → 4.1.6

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 (175) hide show
  1. package/dist/es/index.js +1 -1
  2. package/dist/index.js +1 -1
  3. package/package.json +9 -6
  4. package/.babelrc.js +0 -8
  5. package/.eslintrc.js +0 -12
  6. package/CHANGELOG.md +0 -1146
  7. package/src/README.md +0 -7
  8. package/src/__stories__/editor-page.stories.tsx +0 -65
  9. package/src/__stories__/editor.stories.tsx +0 -134
  10. package/src/__stories__/item-editor.stories.tsx +0 -41
  11. package/src/__stories__/tex-error-view.stories.tsx +0 -26
  12. package/src/__testdata__/input-number.testdata.ts +0 -27
  13. package/src/__tests__/editor.test.tsx +0 -142
  14. package/src/__tests__/hint-editor.test.tsx +0 -83
  15. package/src/__tests__/i18n.test.ts +0 -501
  16. package/src/__tests__/integration.test.ts +0 -5
  17. package/src/__tests__/item-extras-editor.test.tsx +0 -85
  18. package/src/__tests__/traversal.test.ts +0 -377
  19. package/src/all-editors.test.ts +0 -16
  20. package/src/all-editors.ts +0 -83
  21. package/src/all-widgets.ts +0 -7
  22. package/src/article-editor.tsx +0 -438
  23. package/src/components/__stories__/blur-input.stories.tsx +0 -22
  24. package/src/components/__stories__/section-control-button.stories.tsx +0 -26
  25. package/src/components/__tests__/blur-input.test.tsx +0 -67
  26. package/src/components/blur-input.tsx +0 -69
  27. package/src/components/checkbox.tsx +0 -194
  28. package/src/components/device-framer.tsx +0 -90
  29. package/src/components/drag-target.tsx +0 -87
  30. package/src/components/dropdown-option.tsx +0 -311
  31. package/src/components/form-wrapped-text-field.tsx +0 -245
  32. package/src/components/graph-points-count-selector.tsx +0 -31
  33. package/src/components/graph-settings.tsx +0 -627
  34. package/src/components/graph-type-selector.tsx +0 -31
  35. package/src/components/hover-behavior.ts +0 -177
  36. package/src/components/json-editor.tsx +0 -121
  37. package/src/components/link.ts +0 -173
  38. package/src/components/section-control-button.tsx +0 -53
  39. package/src/components/segment-count-selector.tsx +0 -27
  40. package/src/components/simple-button.tsx +0 -85
  41. package/src/components/sortable.tsx +0 -187
  42. package/src/components/util.ts +0 -38
  43. package/src/components/viewport-resizer.tsx +0 -72
  44. package/src/components/widget-editor.tsx +0 -222
  45. package/src/components/widget-select.tsx +0 -48
  46. package/src/diffs/__stories__/perseus-diff-wrapper.tsx +0 -11
  47. package/src/diffs/__stories__/structured-item-diff.stories.tsx +0 -855
  48. package/src/diffs/__stories__/tags-diff.stories.tsx +0 -36
  49. package/src/diffs/__stories__/text-diff.stories.tsx +0 -35
  50. package/src/diffs/__tests__/split-diff.test.ts +0 -33
  51. package/src/diffs/__tests__/string-array-diff.test.ts +0 -73
  52. package/src/diffs/__tests__/structured-item-diff.test.ts +0 -433
  53. package/src/diffs/__tests__/widget-diff-performer.test.ts +0 -43
  54. package/src/diffs/article-diff.tsx +0 -69
  55. package/src/diffs/item-diff.tsx +0 -76
  56. package/src/diffs/renderer-diff.tsx +0 -137
  57. package/src/diffs/split-diff.ts +0 -31
  58. package/src/diffs/string-array-diff.ts +0 -74
  59. package/src/diffs/structured-item-diff.tsx +0 -275
  60. package/src/diffs/tags-diff.tsx +0 -49
  61. package/src/diffs/text-diff.tsx +0 -193
  62. package/src/diffs/widget-diff-performer.ts +0 -69
  63. package/src/diffs/widget-diff.tsx +0 -290
  64. package/src/editor-page.tsx +0 -325
  65. package/src/editor.tsx +0 -1099
  66. package/src/hint-editor.tsx +0 -469
  67. package/src/i18n.ts +0 -140
  68. package/src/iframe-content-renderer.tsx +0 -188
  69. package/src/index.ts +0 -30
  70. package/src/item-editor.tsx +0 -158
  71. package/src/item-extras-editor.tsx +0 -230
  72. package/src/katex-mhchem/index.js +0 -73
  73. package/src/katex-mhchem/parser.js +0 -2659
  74. package/src/multirenderer-editor.tsx +0 -1061
  75. package/src/stateful-article-editor.tsx +0 -173
  76. package/src/stateful-editor-page.tsx +0 -88
  77. package/src/styles/perseus-editor.less +0 -1321
  78. package/src/testing-widgets.ts +0 -11
  79. package/src/tex-error-view.tsx +0 -90
  80. package/src/util/points.test.ts +0 -24
  81. package/src/util/points.ts +0 -13
  82. package/src/util/register-all-widgets-and-editors-for-testing.test.ts +0 -17
  83. package/src/util/register-all-widgets-and-editors-for-testing.ts +0 -17
  84. package/src/version.ts +0 -10
  85. package/src/widgets/__stories__/categorizer-editor.stories.tsx +0 -24
  86. package/src/widgets/__stories__/definition-editor.stories.tsx +0 -18
  87. package/src/widgets/__stories__/dropdown-editor.stories.tsx +0 -18
  88. package/src/widgets/__stories__/explanation-editor.stories.tsx +0 -18
  89. package/src/widgets/__stories__/expression-editor.stories.tsx +0 -122
  90. package/src/widgets/__stories__/image-editor.stories.tsx +0 -72
  91. package/src/widgets/__stories__/input-number-editor.stories.tsx +0 -18
  92. package/src/widgets/__stories__/interaction-editor.stories.tsx +0 -37
  93. package/src/widgets/__stories__/label-image-editor.stories.tsx +0 -99
  94. package/src/widgets/__stories__/matcher-editor.stories.tsx +0 -18
  95. package/src/widgets/__stories__/number-line-editor.stories.tsx +0 -18
  96. package/src/widgets/__stories__/numeric-input-editor.stories.tsx +0 -18
  97. package/src/widgets/__stories__/radio-editor.stories.tsx +0 -116
  98. package/src/widgets/__stories__/sorter-editor.stories.tsx +0 -18
  99. package/src/widgets/__tests__/categorizer-editor.test.tsx +0 -51
  100. package/src/widgets/__tests__/definition-editor.test.tsx +0 -41
  101. package/src/widgets/__tests__/dropdown-editor.test.tsx +0 -57
  102. package/src/widgets/__tests__/explanation-editor.test.tsx +0 -59
  103. package/src/widgets/__tests__/expression-editor.test.tsx +0 -403
  104. package/src/widgets/__tests__/input-number-editor.test.tsx +0 -128
  105. package/src/widgets/__tests__/matcher-editor.test.tsx +0 -47
  106. package/src/widgets/__tests__/number-line-editor.test.tsx +0 -144
  107. package/src/widgets/__tests__/numeric-input-editor.test.tsx +0 -177
  108. package/src/widgets/__tests__/radio-editor.test.tsx +0 -118
  109. package/src/widgets/__tests__/sorter-editor.test.tsx +0 -55
  110. package/src/widgets/categorizer-editor.tsx +0 -104
  111. package/src/widgets/cs-program-editor.tsx +0 -256
  112. package/src/widgets/definition-editor.tsx +0 -77
  113. package/src/widgets/deprecated-standin-editor.tsx +0 -29
  114. package/src/widgets/dropdown-editor.tsx +0 -211
  115. package/src/widgets/example-graphie-widget-editor.tsx +0 -70
  116. package/src/widgets/example-graphie-widget.tsx +0 -172
  117. package/src/widgets/example-widget-editor.tsx +0 -62
  118. package/src/widgets/example-widget.tsx +0 -154
  119. package/src/widgets/explanation-editor.tsx +0 -92
  120. package/src/widgets/expression-editor.tsx +0 -557
  121. package/src/widgets/graded-group-editor.tsx +0 -161
  122. package/src/widgets/graded-group-set-editor.tsx +0 -104
  123. package/src/widgets/grapher-editor.tsx +0 -155
  124. package/src/widgets/group-editor.tsx +0 -83
  125. package/src/widgets/iframe-editor.tsx +0 -202
  126. package/src/widgets/image-editor.tsx +0 -367
  127. package/src/widgets/input-number-editor.tsx +0 -288
  128. package/src/widgets/interaction-editor/constraint-editor.tsx +0 -134
  129. package/src/widgets/interaction-editor/function-editor.tsx +0 -108
  130. package/src/widgets/interaction-editor/index.ts +0 -3
  131. package/src/widgets/interaction-editor/interaction-editor.tsx +0 -749
  132. package/src/widgets/interaction-editor/label-editor.tsx +0 -87
  133. package/src/widgets/interaction-editor/line-editor.tsx +0 -125
  134. package/src/widgets/interaction-editor/mathquill-input.tsx +0 -81
  135. package/src/widgets/interaction-editor/movable-line-editor.tsx +0 -121
  136. package/src/widgets/interaction-editor/movable-point-editor.tsx +0 -89
  137. package/src/widgets/interaction-editor/parametric-editor.tsx +0 -117
  138. package/src/widgets/interaction-editor/point-editor.tsx +0 -72
  139. package/src/widgets/interaction-editor/rectangle-editor.tsx +0 -96
  140. package/src/widgets/interactive-graph-editor.tsx +0 -571
  141. package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +0 -82
  142. package/src/widgets/label-image/__stories__/behavior.stories.tsx +0 -31
  143. package/src/widgets/label-image/__stories__/marker.stories.tsx +0 -75
  144. package/src/widgets/label-image/__stories__/question-markers.stories.tsx +0 -107
  145. package/src/widgets/label-image/__stories__/select-image.stories.tsx +0 -59
  146. package/src/widgets/label-image/answer-choices.tsx +0 -231
  147. package/src/widgets/label-image/behavior.tsx +0 -122
  148. package/src/widgets/label-image/marker.tsx +0 -290
  149. package/src/widgets/label-image/question-markers.tsx +0 -176
  150. package/src/widgets/label-image/select-image.tsx +0 -81
  151. package/src/widgets/label-image-editor.tsx +0 -252
  152. package/src/widgets/lights-puzzle-editor.tsx +0 -278
  153. package/src/widgets/matcher-editor.tsx +0 -149
  154. package/src/widgets/matrix-editor.tsx +0 -156
  155. package/src/widgets/measurer-editor.tsx +0 -253
  156. package/src/widgets/molecule-editor.tsx +0 -64
  157. package/src/widgets/number-line-editor.tsx +0 -512
  158. package/src/widgets/numeric-input-editor.tsx +0 -536
  159. package/src/widgets/orderer-editor.tsx +0 -172
  160. package/src/widgets/passage-editor.tsx +0 -117
  161. package/src/widgets/passage-ref-editor.tsx +0 -80
  162. package/src/widgets/passage-ref-target-editor.tsx +0 -49
  163. package/src/widgets/plotter-editor.tsx +0 -542
  164. package/src/widgets/radio/editor.tsx +0 -398
  165. package/src/widgets/reaction-diagram-editor.tsx +0 -131
  166. package/src/widgets/sequence-editor.tsx +0 -205
  167. package/src/widgets/simple-markdown-tester-editor.tsx +0 -86
  168. package/src/widgets/simple-markdown-tester.tsx +0 -92
  169. package/src/widgets/simulator-editor.tsx +0 -146
  170. package/src/widgets/sorter-editor.tsx +0 -103
  171. package/src/widgets/table-editor.tsx +0 -164
  172. package/src/widgets/unit-editor.tsx +0 -299
  173. package/src/widgets/video-editor.tsx +0 -73
  174. package/tsconfig-build.json +0 -22
  175. package/tsconfig-build.tsbuildinfo +0 -1
@@ -1,1061 +0,0 @@
1
- /**
2
- * Editor for a multi-item question.
3
- *
4
- * TODO(mdr): The UI for managing arrays isn't visually consistent with
5
- * HintsEditor. Should we bring them in line with each other?
6
- */
7
- import {
8
- ApiOptions,
9
- buildEmptyItemTreeForShape,
10
- components,
11
- icons,
12
- itemToTree,
13
- MultiItems,
14
- } from "@khanacademy/perseus";
15
- import {StyleSheet, css} from "aphrodite";
16
- // eslint-disable-next-line import/no-extraneous-dependencies
17
- import lens from "hubble";
18
- import * as React from "react";
19
- import ReactDOM from "react-dom";
20
-
21
- import JsonEditor from "./components/json-editor";
22
- import SimpleButton from "./components/simple-button";
23
- import Editor from "./editor";
24
- import {HintEditor} from "./hint-editor";
25
-
26
- import type {
27
- APIOptions,
28
- ChangeHandler,
29
- EditorMode,
30
-
31
- // Multi-item item types
32
- Item,
33
- // ItemTree is used below, the linter is confused.
34
- ItemTree, // eslint-disable-line @typescript-eslint/no-unused-vars
35
- ItemObjectNode,
36
- ItemArrayNode,
37
- ContentNode,
38
- HintNode,
39
- TagsNode,
40
-
41
- // Multi-item shape types
42
- Shape,
43
- ArrayShape,
44
- ObjectShape,
45
- ContentShape,
46
- HintShape,
47
- TagsShape,
48
- } from "@khanacademy/perseus";
49
-
50
- const {InlineIcon} = components;
51
- const {iconChevronDown, iconTrash} = icons;
52
- const {MultiRenderer} = MultiItems;
53
-
54
- // TODO(CP-4849): figure out when $ReadOnlyArray<number> vs $ReadOnlyArray<string> should be used
55
- type Path = ReadonlyArray<any>;
56
-
57
- type ModeDropdownProps = {
58
- currentMode: EditorMode;
59
- // A function that takes in a string signifying the mode (ex: "edit")
60
- onChange: (mode: EditorMode) => unknown;
61
- };
62
-
63
- /**
64
- * Component that displays the mode dropdown.
65
- *
66
- * The mode dropdown is the selector at the top of the editor that lets you
67
- * switch between edit, preview, and dev-only JSON mode.
68
- */
69
- class ModeDropdown extends React.Component<ModeDropdownProps> {
70
- _handleSelectMode = (event: React.ChangeEvent<HTMLSelectElement>) => {
71
- if (this.props.onChange) {
72
- // event.target.value corresponds to the options' values below which
73
- // are limited to EditorMode, but TypeScript doesn't know that so we have
74
- // to cast through any here.
75
- const value = event.target.value as EditorMode;
76
- this.props.onChange(value);
77
- }
78
- };
79
-
80
- render(): React.ReactNode {
81
- return (
82
- <label>
83
- Mode:{" "}
84
- <select
85
- value={this.props.currentMode}
86
- onChange={this._handleSelectMode}
87
- >
88
- <option value="edit">Edit</option>
89
- <option value="preview">Preview</option>
90
- <option value="json">Dev-only JSON</option>
91
- </select>
92
- </label>
93
- );
94
- }
95
- }
96
-
97
- /**
98
- * Convert a camel-cased string to a human-formatted string.
99
- * "superCoolThings" -> "super cool things"
100
- */
101
- function camelCaseToHuman(str: string) {
102
- // Decapitalize the capital letters, and add a space before each.
103
- return str.replace(/[A-Z]/g, (s) => " " + s.toLowerCase());
104
- }
105
-
106
- /**
107
- * Capitalize the first letter of the given string.
108
- * "super cool things" -> "Super cool things"
109
- */
110
- function capitalize(str: string) {
111
- return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
112
- }
113
-
114
- /**
115
- * Convert the given pluralized word to a singularized word.
116
- * "super cool things" -> "super cool thing"
117
- */
118
- function pluralToSingular(str: string) {
119
- if (str.charAt(str.length - 1) === "s") {
120
- // Incredibly weak implementation :P
121
- return str.slice(0, -1);
122
- }
123
- // Uh oh, dunno how to singularize anything but the simplest case!
124
- // Let's just return the plural form, and hope the user forgives the
125
- // grammatical inconsistency.
126
- return str;
127
- }
128
-
129
- /**
130
- * When iterating through the editors, we don't keep track of the extra
131
- * `_multi` part at the beginning. This is a helper function which takes a path
132
- * and prepends that key.
133
- */
134
- function multiPath(path: Path | Array<any>) {
135
- return ["_multi", ...path];
136
- }
137
-
138
- // Return an h1 if depth=0, h2 if depth=1, etc.
139
- // NOTE: This component accepts pass-through props.
140
- type HeaderProps = {
141
- depth: number;
142
- };
143
-
144
- const Header = ({depth, ...props}: HeaderProps): React.ReactElement => {
145
- const headerLevel = Math.min(depth, 5) + 1;
146
- const HeaderTag = `h${headerLevel}`;
147
- return <HeaderTag {...props} />;
148
- };
149
-
150
- // Actions is inexact so that we can pass an instance MultiRendererEditor.
151
- interface Actions {
152
- addArrayElement: (path: Path, shape: Shape) => void;
153
- mergeValueAtPath: (path: Path, newValue?: any) => void;
154
- setValueAtPath: (path: Path, newValue?: any) => void;
155
- moveArrayElementDown: (path: Path) => void;
156
- moveArrayElementUp: (path: Path) => void;
157
- removeArrayElement: (path: Path) => void;
158
- }
159
-
160
- // This type is used to define prop types for various nodes. The S and D type
161
- // params should match when used, e.g. the prop types for `HintNodeContent` is
162
- // `NodePropTypes<HintShape, HintNode>`.
163
- type NodePropTypes<S = Shape, D = ItemTree> = {
164
- shape: S;
165
- data: D;
166
- path: Path;
167
- actions: Actions;
168
- apiOptions: APIOptions;
169
- // For the left-hand column, we use edit mode and leave renderers empty.
170
- // For the right-hand column, we use preview mode and provide renderers
171
- // via a MultiRenderer.
172
- // TODO(CP-4850): figure out how to type this, it appears to be a tree where the
173
- // leaf nodes could be typed using RendererInterface.
174
- renderers?: any;
175
- };
176
-
177
- /**
178
- * Render a node in the editor tree, given the shape of the target
179
- * node, the data stored in the target node, the path to the target
180
- * node, and any UI controls that affect how this node relates to its
181
- * parent (e.g. remove from parent array).
182
- *
183
- * This returns a container element with a pretty title and additional
184
- * UI controls for this node. Its contents are produced by
185
- * `NodeContent`. The two functions are mutually recursive.
186
- *
187
- * Leaf nodes, like items and hints, render an editor pod around their
188
- * content. Container nodes, like arrays and objects, render a header above
189
- * their content.
190
- */
191
- const NodeContainer = (
192
- props: NodePropTypes & {
193
- controls?: ReadonlyArray<React.ReactNode>;
194
- name?: string;
195
- },
196
- ) => {
197
- const {
198
- shape,
199
- data,
200
- path,
201
- actions,
202
- name: givenName,
203
- controls,
204
- ...otherProps
205
- } = props;
206
-
207
- const name = givenName || camelCaseToHuman(path[path.length - 1] || "");
208
-
209
- const children = (
210
- <NodeContent
211
- {...otherProps}
212
- shape={shape}
213
- data={data}
214
- path={path}
215
- actions={actions}
216
- />
217
- );
218
-
219
- const key = path.join(".");
220
-
221
- if (shape.type === "array") {
222
- return (
223
- <ArrayContainer
224
- key={key}
225
- name={name}
226
- controls={controls}
227
- path={path}
228
- shape={shape}
229
- actions={actions}
230
- >
231
- {children}
232
- </ArrayContainer>
233
- );
234
- }
235
- if (shape.type === "object") {
236
- return (
237
- <ObjectContainer
238
- key={key}
239
- name={name}
240
- controls={controls}
241
- path={path}
242
- >
243
- {children}
244
- </ObjectContainer>
245
- );
246
- }
247
- return (
248
- <LeafContainer
249
- key={key}
250
- name={name}
251
- controls={controls}
252
- path={path}
253
- shape={shape}
254
- >
255
- {children}
256
- </LeafContainer>
257
- );
258
- };
259
-
260
- type LeafContainerProps = {
261
- name: string;
262
- controls?: React.ReactNode;
263
- children?: React.ReactNode;
264
- path: Path;
265
- shape: Shape;
266
- };
267
- const LeafContainer = ({
268
- name,
269
- controls,
270
- children,
271
- path,
272
- shape,
273
- }: LeafContainerProps): React.ReactElement => {
274
- const hasPreviewHeading = shape.type === "content" || shape.type === "hint";
275
- const previewHeading = hasPreviewHeading && (
276
- <div className={css(styles.containerHeader)}>
277
- {/* @ts-expect-error - TS2322 - Type '{ children: string; depth: number; className: string; }' is not assignable to type 'IntrinsicAttributes & HeaderProps & { children?: ReactNode; }'. */}
278
- <Header depth={path.length} className={css(styles.containerTitle)}>
279
- {capitalize(name)}
280
- </Header>
281
- </div>
282
- );
283
- return (
284
- <div className={css(styles.container)}>
285
- <span className={css(styles.row, styles.rowHeading)}>
286
- <div className={css(styles.columnLeft)}>
287
- <div className={"pod-title " + css(styles.containerHeader)}>
288
- <div className={css(styles.containerTitle)}>
289
- {capitalize(name)}
290
- </div>
291
- {controls}
292
- </div>
293
- </div>
294
- <div className={css(styles.columnRight)}>{previewHeading}</div>
295
- </span>
296
- {children}
297
- </div>
298
- );
299
- };
300
-
301
- interface ArrayContainerActions {
302
- addArrayElement: (path: Path, shape: Shape) => void;
303
- }
304
-
305
- type ArrayContainerProps = {
306
- name: string;
307
- controls?: React.ReactNode;
308
- children?: React.ReactNode;
309
- path: Path;
310
- shape: ArrayShape;
311
- actions: ArrayContainerActions;
312
- };
313
- const ArrayContainer = (props: ArrayContainerProps): React.ReactElement => {
314
- const {name, controls, children, path, shape, actions} = props;
315
- return (
316
- <div className={css(styles.container)}>
317
- {controls && (
318
- <div className={css(styles.columnLeft, styles.containerHeader)}>
319
- {controls}
320
- </div>
321
- )}
322
- <div>{children}</div>
323
- <div className={css(styles.columnLeft)}>
324
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
325
- <a
326
- href="javascript:void 0"
327
- onClick={() =>
328
- actions.addArrayElement(path, shape.elementShape)
329
- }
330
- >
331
- Add a {pluralToSingular(name)}
332
- </a>
333
- </div>
334
- </div>
335
- );
336
- };
337
-
338
- type ObjectContainerProps = {
339
- name: string;
340
- controls?: React.ReactNode;
341
- children?: React.ReactNode;
342
- path: Path;
343
- };
344
- const ObjectContainer = ({
345
- name,
346
- controls,
347
- children,
348
- path,
349
- }: ObjectContainerProps): React.ReactElement => {
350
- const headingEditor = (
351
- <div className={css(styles.containerHeader)}>
352
- {/* @ts-expect-error - TS2322 - Type '{ children: string; depth: number; className: string; }' is not assignable to type 'IntrinsicAttributes & HeaderProps & { children?: ReactNode; }'. */}
353
- <Header depth={path.length} className={css(styles.containerTitle)}>
354
- {capitalize(name)}
355
- </Header>
356
- {controls}
357
- </div>
358
- );
359
- const headingPreview = (name || controls) && (
360
- <div
361
- className={css(
362
- styles.containerHeader,
363
- styles.previewCollectionHeader,
364
- )}
365
- >
366
- {/* @ts-expect-error - TS2322 - Type '{ children: string; depth: number; className: string; }' is not assignable to type 'IntrinsicAttributes & HeaderProps & { children?: ReactNode; }'. */}
367
- <Header depth={path.length} className={css(styles.containerTitle)}>
368
- {capitalize(name)}
369
- </Header>
370
- </div>
371
- );
372
- const hasBothHeadings = headingEditor && headingPreview;
373
- return (
374
- <div className={css(styles.container)}>
375
- {hasBothHeadings && (
376
- <span className={css(styles.row)}>
377
- <div className={css(styles.columnLeft)}>
378
- {headingEditor}
379
- </div>
380
- <div className={css(styles.columnRight)}>
381
- {headingPreview}
382
- </div>
383
- </span>
384
- )}
385
- <div className={css(path.length > 0 && styles.contentIndent)}>
386
- {children}
387
- </div>
388
- </div>
389
- );
390
- };
391
-
392
- /**
393
- * Render the content of node in the editor tree, given the shape of
394
- * the target node, the data stored in the target node, and the path to
395
- * the target node.
396
- *
397
- * If the target node is a leaf, this returns an editor. Otherwise, it
398
- * iterates over the child nodes, and outputs `NodeContainer` for
399
- * each of them. The two functions are mutually recursive.
400
- */
401
- const NodeContent = (props: NodePropTypes) => {
402
- const {shape, data, ...restProps} = props;
403
-
404
- // All uses of `data` have been cast through any which isn't safe. This was
405
- // done to avoid introducing new logic which may have resulted in a change
406
- // of behavior. Also, there doesn't appear to be a way to tell TypeScript the
407
- // difference between and `TagsNode` and `ItemArrayNode`, see
408
- // perseus-all-package/multi-items/item-types.js.
409
- if (shape.type === "content") {
410
- return (
411
- <ItemNodeContent
412
- shape={shape}
413
- data={data as ContentNode}
414
- {...restProps}
415
- />
416
- );
417
- }
418
- if (shape.type === "hint") {
419
- return (
420
- <HintNodeContent
421
- shape={shape}
422
- data={data as HintNode}
423
- {...restProps}
424
- />
425
- );
426
- }
427
- if (shape.type === "tags") {
428
- return (
429
- <TagsNodeContent
430
- shape={shape}
431
- data={data as TagsNode}
432
- {...restProps}
433
- />
434
- );
435
- }
436
- if (shape.type === "array") {
437
- return (
438
- <ArrayNodeContent
439
- shape={shape}
440
- data={data as ItemArrayNode}
441
- {...restProps}
442
- />
443
- );
444
- }
445
- if (shape.type === "object") {
446
- return (
447
- <ObjectNodeContent
448
- shape={shape}
449
- data={data as ItemObjectNode}
450
- {...restProps}
451
- />
452
- );
453
- }
454
- return null;
455
- };
456
-
457
- type WithStickinessProps = {
458
- sticky: boolean;
459
- };
460
-
461
- type WithStickiness<T> = T & WithStickinessProps;
462
-
463
- /**
464
- * HOC that adds a "sticky" prop to the wrapped component that is true
465
- * when the rendered component is taller than the window. Since sticky content
466
- * can be somewhat distracting, we'd like to avoid it when not useful. This
467
- * HOC is useful for only making content sticky when useful.
468
- *
469
- * It does so by polling the height and comparing it to the window height.
470
- */
471
- const withStickiness = <
472
- Config extends Record<any, any>,
473
- Component extends React.ComponentType<WithStickiness<Config>>,
474
- >(
475
- WrappedComponent: Component,
476
- ): React.ComponentType<Config> => {
477
- type State = {
478
- sticky: boolean;
479
- };
480
- return class StickyComponent extends React.Component<Config, State> {
481
- // @ts-expect-error - TS2564 - Property 'stickynessTimer' has no initializer and is not definitely assigned in the constructor.
482
- stickynessTimer: number;
483
-
484
- state = {
485
- sticky: false,
486
- };
487
-
488
- componentDidMount() {
489
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
490
- // eslint-disable-next-line no-restricted-syntax
491
- // @ts-expect-error - TS2322 - Type 'Timer' is not assignable to type 'number'.
492
- this.stickynessTimer = setInterval(this.updateStickiness, 1000);
493
- this.updateStickiness();
494
- }
495
-
496
- componentWillUnmount() {
497
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
498
- // eslint-disable-next-line no-restricted-syntax
499
- clearInterval(this.stickynessTimer);
500
- }
501
-
502
- updateStickiness = () => {
503
- const domNode = ReactDOM.findDOMNode(this);
504
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'offsetHeight' does not exist on type 'Element | Text'.
505
- const height = domNode.offsetHeight;
506
- const windowHeight = window.innerHeight;
507
- const sticky = height > windowHeight;
508
- if (sticky !== this.state.sticky) {
509
- this.setState({
510
- sticky,
511
- });
512
- }
513
- };
514
-
515
- render(): React.ReactNode {
516
- return (
517
- // @ts-expect-error - TS2322 - Type 'Readonly<Config> & { sticky: boolean; children?: ReactNode; }' is not assignable to type 'IntrinsicAttributes & LibraryManagedAttributes<Component, PropsWithChildren<WithStickiness<Config>>>'.
518
- <WrappedComponent {...this.props} sticky={this.state.sticky} />
519
- );
520
- }
521
- };
522
- };
523
-
524
- const ItemNodeContent = withStickiness(
525
- (props: NodePropTypes<ContentShape, ContentNode> & WithStickinessProps) => {
526
- const {data, path, actions, apiOptions, renderers, sticky} = props;
527
-
528
- const preview = (
529
- <div className="framework-perseus">{lens(renderers).get(path)}</div>
530
- );
531
-
532
- return (
533
- <span>
534
- <div className={css(styles.row)}>
535
- <div className={css(styles.columnLeft)}>
536
- <div className={css(sticky && styles.sticky)}>
537
- {/* TODO(CP-4852): only pass the props to Editor that it uses. */}
538
- {
539
- // @ts-expect-error - TS2769 - No overload matches this call.
540
- <Editor
541
- {...data}
542
- onChange={(newVal) =>
543
- actions.mergeValueAtPath(path, newVal)
544
- }
545
- apiOptions={apiOptions}
546
- />
547
- }
548
- </div>
549
- </div>
550
- <div className={css(styles.columnRight)}>
551
- <div className={css(sticky && styles.sticky)}>
552
- {preview}
553
- </div>
554
- </div>
555
- </div>
556
- </span>
557
- );
558
- },
559
- );
560
-
561
- const HintNodeContent = withStickiness(
562
- (props: NodePropTypes<HintShape, HintNode> & WithStickinessProps) => {
563
- const {data, path, actions, apiOptions, renderers, sticky} = props;
564
-
565
- const preview = (
566
- <div className="framework-perseus">{lens(renderers).get(path)}</div>
567
- );
568
-
569
- return (
570
- <div className={css(styles.row)}>
571
- <div className={css(styles.columnLeft)}>
572
- <div className={css(sticky && styles.sticky)}>
573
- <HintEditor
574
- {...data}
575
- className={css(styles.hintEditor)}
576
- onChange={(newVal) =>
577
- actions.mergeValueAtPath(path, newVal)
578
- }
579
- apiOptions={apiOptions}
580
- showTitle={false}
581
- showRemoveButton={false}
582
- showMoveButtons={false}
583
- // no-op handler since showRemoveButton={false}
584
- onRemove={() => {}}
585
- // no-op handler since showMoveButtons={false}
586
- onMove={(direction: number) => {}}
587
- isFirst={true}
588
- isLast={true}
589
- />
590
- </div>
591
- </div>
592
- <div className={css(styles.columnRight)}>
593
- <div className={css(sticky && styles.sticky)}>
594
- {preview}
595
- </div>
596
- </div>
597
- </div>
598
- );
599
- },
600
- );
601
-
602
- const TagsNodeContent = (props: NodePropTypes<TagsShape, TagsNode>) => {
603
- const {data, path, actions, apiOptions} = props;
604
- const {GroupMetadataEditor} = apiOptions;
605
-
606
- if (GroupMetadataEditor == null) {
607
- return null;
608
- }
609
- return (
610
- <div className={css(styles.columnLeft)}>
611
- <div className={css(styles.tagsEditor)}>
612
- <GroupMetadataEditor
613
- value={data}
614
- onChange={(newVal) => actions.setValueAtPath(path, newVal)}
615
- showTitle={false}
616
- />
617
- </div>
618
- </div>
619
- );
620
- };
621
-
622
- const ArrayNodeContent = (props: NodePropTypes<ArrayShape, ItemArrayNode>) => {
623
- const {shape, data, path, actions, ...otherProps} = props;
624
-
625
- const collectionName = camelCaseToHuman(path[path.length - 1]);
626
- const elementName = pluralToSingular(collectionName);
627
-
628
- const elementType = shape.elementShape.type;
629
- const elementIsLeaf = elementType === "content" || elementType === "hint";
630
-
631
- /**
632
- * TODO(somewhatabstract, JIRA-XXXX):
633
- * The NodePropTypes generic and specifically the ItemArrayNode could
634
- * contain a variety of types that are not arrays. Probably need to refine
635
- * the type more before doing this work, or rework the `ArrayNodeContent`
636
- * component to be less permissive about the props it accepts.
637
- */
638
- const children = data.map((subdata, i) => {
639
- const subpath = path.concat(i);
640
- const controls = [
641
- i > 0 && (
642
- <div key="moveArrayElementUp" className={css(styles.control)}>
643
- <SimpleButton
644
- color="orange"
645
- title="Move up"
646
- onClick={() => actions.moveArrayElementUp(subpath)}
647
- >
648
- <div className={css(styles.verticalFlip)}>
649
- <InlineIcon {...iconChevronDown} />
650
- </div>
651
- </SimpleButton>
652
- </div>
653
- ),
654
- i < data.length - 1 && (
655
- <div key="moveArrayElementDown" className={css(styles.control)}>
656
- <SimpleButton
657
- color="orange"
658
- title="Move down"
659
- onClick={() => actions.moveArrayElementDown(subpath)}
660
- >
661
- <InlineIcon {...iconChevronDown} />
662
- </SimpleButton>
663
- </div>
664
- ),
665
- <div key="removeArrayElement" className={css(styles.control)}>
666
- <SimpleButton
667
- color="orange"
668
- title="Delete"
669
- onClick={() => actions.removeArrayElement(subpath)}
670
- >
671
- <InlineIcon {...iconTrash} />
672
- </SimpleButton>
673
- </div>,
674
- ];
675
-
676
- return (
677
- <div
678
- key={i}
679
- className={css(
680
- styles.arrayElement,
681
- !elementIsLeaf && styles.arrayElementAndNotLeaf,
682
- )}
683
- >
684
- <NodeContainer
685
- {...otherProps}
686
- key={i}
687
- shape={shape.elementShape}
688
- data={subdata}
689
- path={subpath}
690
- actions={actions}
691
- name={`${elementName} ${i + 1}`}
692
- controls={controls}
693
- />
694
- </div>
695
- );
696
- });
697
-
698
- return <div>{children}</div>;
699
- };
700
-
701
- const ObjectNodeContent = (
702
- props: NodePropTypes<ObjectShape, ItemObjectNode>,
703
- ) => {
704
- const {shape, data, path, ...otherProps} = props;
705
-
706
- // Object iteration order should automatically match the order in which the
707
- // keys were defined in the object literal. So, whatever order semantically
708
- // made sense to the shape's author is the order in which we'll iterate :)
709
- const children = Object.keys(shape.shape).map((subkey) => (
710
- <div key={subkey} className={css(styles.objectElement)}>
711
- <NodeContainer
712
- {...otherProps}
713
- shape={shape.shape[subkey]}
714
- data={data[subkey]}
715
- path={path.concat(subkey)}
716
- />
717
- </div>
718
- ));
719
-
720
- return <div>{children}</div>;
721
- };
722
-
723
- interface LayoutStatics {
724
- shape: Shape;
725
- }
726
-
727
- type MultiRendererEditorProps = {
728
- // eslint-disable-next-line no-restricted-syntax
729
- Layout: React.ComponentType<any> & LayoutStatics;
730
- apiOptions: APIOptions;
731
- item: Item;
732
- editorMode: EditorMode;
733
- onChange: ChangeHandler;
734
- };
735
-
736
- class MultiRendererEditor extends React.Component<MultiRendererEditorProps> {
737
- layout: React.ElementRef<any> | null | undefined;
738
-
739
- _renderLayout: () => React.ReactElement = () => {
740
- const {Layout, apiOptions, item} = this.props;
741
-
742
- return (
743
- <Layout
744
- ref={(node) => (this.layout = node)}
745
- item={item}
746
- apiOptions={apiOptions}
747
- />
748
- );
749
- };
750
-
751
- _renderJson: () => React.ReactElement<React.ComponentProps<"div">> = () => {
752
- return (
753
- <div>
754
- <ModeDropdown
755
- currentMode={this.props.editorMode}
756
- onChange={(editorMode) => this.props.onChange({editorMode})}
757
- />
758
- <JsonEditor
759
- multiLine
760
- value={this.props.item}
761
- onChange={(item) => this.props.onChange({item})}
762
- />
763
- </div>
764
- );
765
- };
766
-
767
- _renderPreview: () => React.ReactElement<React.ComponentProps<"div">> =
768
- () => {
769
- return (
770
- <div>
771
- <ModeDropdown
772
- currentMode={this.props.editorMode}
773
- onChange={(editorMode) =>
774
- this.props.onChange({editorMode})
775
- }
776
- />
777
- {this._renderLayout()}
778
- </div>
779
- );
780
- };
781
-
782
- mergeValueAtPath: (path: Path, newValue?: any) => void = (
783
- path: Path,
784
- newValue: unknown,
785
- ) => {
786
- this.props.onChange({
787
- item: lens(this.props.item)
788
- .merge(multiPath(path), newValue)
789
- .freeze(),
790
- });
791
- };
792
-
793
- setValueAtPath: (path: Path, newValue?: any) => void = (
794
- path: Path,
795
- newValue: unknown,
796
- ) => {
797
- this.props.onChange({
798
- item: lens(this.props.item).set(multiPath(path), newValue).freeze(),
799
- });
800
- };
801
-
802
- addArrayElement: (path: Path, shape: Shape) => void = (
803
- path: Path,
804
- shape: Shape,
805
- ) => {
806
- const currentLength = lens(this.props.item).get(multiPath(path)).length;
807
- const newElementPath = path.concat(currentLength);
808
- const newValue = buildEmptyItemTreeForShape(shape);
809
- this.props.onChange({
810
- item: lens(this.props.item)
811
- .set(multiPath(newElementPath), newValue)
812
- .freeze(),
813
- });
814
- };
815
-
816
- removeArrayElement: (path: Path) => void = (path: Path) => {
817
- this.props.onChange({
818
- item: lens(this.props.item).del(multiPath(path)).freeze(),
819
- });
820
- };
821
-
822
- moveArrayElementDown: (path: Path) => void = (path: Path) => {
823
- // Moving an element down can also be expressed as swapping it with the
824
- // following element.
825
- const index = path[path.length - 1];
826
- const nextElementIndex = index + 1;
827
- const nextElementPath = path.slice(0, -1).concat(nextElementIndex);
828
-
829
- const element = lens(this.props.item).get(multiPath(path));
830
- const nextElement = lens(this.props.item).get(
831
- multiPath(nextElementPath),
832
- );
833
-
834
- this.props.onChange({
835
- item: lens(this.props.item)
836
- .set(multiPath(path), nextElement)
837
- .set(multiPath(nextElementPath), element)
838
- .freeze(),
839
- });
840
- };
841
-
842
- moveArrayElementUp: (path: Path) => void = (path: Path) => {
843
- // Moving an element up can also be expressed as moving the previous
844
- // element down.
845
- const index = path[path.length - 1];
846
- const previousElementPath = path.slice(0, -1).concat(index - 1);
847
- this.moveArrayElementDown(previousElementPath);
848
- };
849
-
850
- _renderEdit: () => React.ReactElement<React.ComponentProps<"div">> = () => {
851
- const apiOptions = {
852
- ...ApiOptions.defaults,
853
- ...this.props.apiOptions,
854
- } as const;
855
-
856
- const {item} = this.props;
857
- const itemShape: Shape = this.props.Layout.shape;
858
-
859
- return (
860
- <div className="perseus-multirenderer-editor">
861
- <ModeDropdown
862
- currentMode={this.props.editorMode}
863
- onChange={(editorMode) => this.props.onChange({editorMode})}
864
- />
865
-
866
- <MultiRenderer
867
- item={item}
868
- shape={itemShape}
869
- apiOptions={apiOptions}
870
- // Today, with analytics being the only thing in
871
- // dependencies, we send in a dummy function as we don't
872
- // want to gather analytics events from within the editor.
873
- dependencies={{
874
- analytics: {onAnalyticsEvent: async () => {}},
875
- }}
876
- >
877
- {({renderers}) => (
878
- <NodeContainer
879
- shape={itemShape}
880
- data={itemToTree(item)}
881
- path={[]}
882
- actions={this}
883
- apiOptions={apiOptions}
884
- renderers={renderers}
885
- />
886
- )}
887
- </MultiRenderer>
888
- </div>
889
- );
890
- };
891
-
892
- score: () => any | undefined = () => {
893
- if (this.layout) {
894
- // @ts-expect-error - TS2571 - Object is of type 'unknown'.
895
- return this.layout.score();
896
- }
897
- };
898
-
899
- getSerializedState: () => any | undefined = () => {
900
- if (this.layout) {
901
- // @ts-expect-error - TS2571 - Object is of type 'unknown'.
902
- return this.layout.getSerializedState();
903
- }
904
- };
905
-
906
- restoreSerializedState: (state?: any) => void = (state: any) => {
907
- if (this.layout) {
908
- // @ts-expect-error - TS2571 - Object is of type 'unknown'.
909
- this.layout.restoreSerializedState(state);
910
- }
911
- };
912
-
913
- _renderContent: () =>
914
- | React.ReactElement<React.ComponentProps<"div">>
915
- | React.ReactNode = () => {
916
- switch (this.props.editorMode) {
917
- case "json":
918
- return this._renderJson();
919
- case "preview":
920
- return this._renderPreview();
921
- case "edit":
922
- return this._renderEdit();
923
- default:
924
- return (
925
- <ModeDropdown
926
- currentMode={this.props.editorMode}
927
- onChange={(editorMode) =>
928
- this.props.onChange({
929
- editorMode,
930
- })
931
- }
932
- />
933
- );
934
- }
935
- };
936
-
937
- render(): React.ReactNode {
938
- return <div id="perseus">{this._renderContent()}</div>;
939
- }
940
- }
941
-
942
- const styles = StyleSheet.create({
943
- // This is used in a number of places throughout this file.
944
- container: {},
945
-
946
- // eslint-disable-next-line react-native/no-unused-styles
947
- editor: {
948
- width: "100%",
949
- },
950
-
951
- // eslint-disable-next-line react-native/no-unused-styles
952
- treePreview: {
953
- position: "relative",
954
- },
955
-
956
- verticalFlip: {
957
- transform: "scaleY(-1)",
958
- },
959
-
960
- control: {
961
- marginLeft: 12,
962
- },
963
-
964
- containerHeader: {
965
- alignItems: "flex-end",
966
- display: "flex",
967
- flexDirection: "row",
968
- },
969
-
970
- previewCollectionHeader: {
971
- marginBottom: 16,
972
- },
973
-
974
- containerTitle: {
975
- flexGrow: 1,
976
- margin: 0,
977
- },
978
-
979
- contentIndent: {
980
- marginLeft: 8,
981
- },
982
-
983
- hintEditor: {
984
- paddingBottom: 0,
985
- },
986
-
987
- arrayElement: {
988
- marginBottom: 16,
989
- },
990
-
991
- // Leaf nodes are already wrapped in cute little pods, so they don't need
992
- // this extra border between array elements.
993
- arrayElementAndNotLeaf: {
994
- borderBottom: "1px solid #ccc",
995
- ":first-child": {
996
- borderTop: "1px solid #ccc",
997
- paddingTop: 16,
998
- },
999
- },
1000
-
1001
- objectElement: {
1002
- marginBottom: 16,
1003
- },
1004
-
1005
- tagsEditor: {
1006
- border: "1px solid #ddd",
1007
- padding: "5px 10px",
1008
- },
1009
-
1010
- /**
1011
- * A row contains a fixed width editor and a preview that expands as
1012
- * needed.
1013
- */
1014
- row: {
1015
- display: "flex",
1016
- position: "relative",
1017
- },
1018
-
1019
- /**
1020
- * The editor.
1021
- */
1022
- columnLeft: {
1023
- width: 360,
1024
- marginRight: 30,
1025
- // so that the `position: absolute` of line markers are positioned
1026
- // relative to this.
1027
- position: "relative",
1028
- },
1029
-
1030
- /**
1031
- * The preview.
1032
- */
1033
- columnRight: {
1034
- flex: 1,
1035
- marginLeft: 30,
1036
- position: "relative",
1037
- },
1038
-
1039
- /**
1040
- * Sticks to just under the heading.
1041
- */
1042
- sticky: {
1043
- position: "sticky",
1044
- top: 33, // height of the cute pod for the editor
1045
- },
1046
-
1047
- /**
1048
- * Used for sticky headings.
1049
- */
1050
- rowHeading: {
1051
- position: "sticky",
1052
- backgroundColor: "white",
1053
- width: "100%",
1054
- // TODO(joshuan): Make this less arbitrary. It should be higher than
1055
- // perseus content.
1056
- zIndex: 101,
1057
- top: -1,
1058
- },
1059
- });
1060
-
1061
- export default MultiRendererEditor;