@khanacademy/perseus-editor 4.1.5 → 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.
- package/dist/es/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -6
- package/.babelrc.js +0 -8
- package/.eslintrc.js +0 -12
- package/CHANGELOG.md +0 -1155
- package/src/README.md +0 -7
- package/src/__stories__/editor-page.stories.tsx +0 -65
- package/src/__stories__/editor.stories.tsx +0 -134
- package/src/__stories__/item-editor.stories.tsx +0 -41
- package/src/__stories__/tex-error-view.stories.tsx +0 -26
- package/src/__testdata__/input-number.testdata.ts +0 -27
- package/src/__tests__/editor.test.tsx +0 -142
- package/src/__tests__/hint-editor.test.tsx +0 -83
- package/src/__tests__/i18n.test.ts +0 -501
- package/src/__tests__/integration.test.ts +0 -5
- package/src/__tests__/item-extras-editor.test.tsx +0 -85
- package/src/__tests__/traversal.test.ts +0 -377
- package/src/all-editors.test.ts +0 -16
- package/src/all-editors.ts +0 -83
- package/src/all-widgets.ts +0 -7
- package/src/article-editor.tsx +0 -438
- package/src/components/__stories__/blur-input.stories.tsx +0 -22
- package/src/components/__stories__/section-control-button.stories.tsx +0 -26
- package/src/components/__tests__/blur-input.test.tsx +0 -67
- package/src/components/blur-input.tsx +0 -69
- package/src/components/checkbox.tsx +0 -194
- package/src/components/device-framer.tsx +0 -90
- package/src/components/drag-target.tsx +0 -87
- package/src/components/dropdown-option.tsx +0 -311
- package/src/components/form-wrapped-text-field.tsx +0 -245
- package/src/components/graph-points-count-selector.tsx +0 -31
- package/src/components/graph-settings.tsx +0 -627
- package/src/components/graph-type-selector.tsx +0 -31
- package/src/components/hover-behavior.ts +0 -177
- package/src/components/json-editor.tsx +0 -121
- package/src/components/link.ts +0 -173
- package/src/components/section-control-button.tsx +0 -53
- package/src/components/segment-count-selector.tsx +0 -27
- package/src/components/simple-button.tsx +0 -85
- package/src/components/sortable.tsx +0 -187
- package/src/components/util.ts +0 -38
- package/src/components/viewport-resizer.tsx +0 -72
- package/src/components/widget-editor.tsx +0 -222
- package/src/components/widget-select.tsx +0 -48
- package/src/diffs/__stories__/perseus-diff-wrapper.tsx +0 -11
- package/src/diffs/__stories__/structured-item-diff.stories.tsx +0 -855
- package/src/diffs/__stories__/tags-diff.stories.tsx +0 -36
- package/src/diffs/__stories__/text-diff.stories.tsx +0 -35
- package/src/diffs/__tests__/split-diff.test.ts +0 -33
- package/src/diffs/__tests__/string-array-diff.test.ts +0 -73
- package/src/diffs/__tests__/structured-item-diff.test.ts +0 -433
- package/src/diffs/__tests__/widget-diff-performer.test.ts +0 -43
- package/src/diffs/article-diff.tsx +0 -69
- package/src/diffs/item-diff.tsx +0 -76
- package/src/diffs/renderer-diff.tsx +0 -137
- package/src/diffs/split-diff.ts +0 -31
- package/src/diffs/string-array-diff.ts +0 -74
- package/src/diffs/structured-item-diff.tsx +0 -275
- package/src/diffs/tags-diff.tsx +0 -49
- package/src/diffs/text-diff.tsx +0 -193
- package/src/diffs/widget-diff-performer.ts +0 -69
- package/src/diffs/widget-diff.tsx +0 -290
- package/src/editor-page.tsx +0 -325
- package/src/editor.tsx +0 -1099
- package/src/hint-editor.tsx +0 -469
- package/src/i18n.ts +0 -140
- package/src/iframe-content-renderer.tsx +0 -188
- package/src/index.ts +0 -30
- package/src/item-editor.tsx +0 -158
- package/src/item-extras-editor.tsx +0 -230
- package/src/katex-mhchem/index.js +0 -73
- package/src/katex-mhchem/parser.js +0 -2659
- package/src/multirenderer-editor.tsx +0 -1061
- package/src/stateful-article-editor.tsx +0 -173
- package/src/stateful-editor-page.tsx +0 -88
- package/src/styles/perseus-editor.less +0 -1321
- package/src/testing-widgets.ts +0 -11
- package/src/tex-error-view.tsx +0 -90
- package/src/util/points.test.ts +0 -24
- package/src/util/points.ts +0 -13
- package/src/util/register-all-widgets-and-editors-for-testing.test.ts +0 -17
- package/src/util/register-all-widgets-and-editors-for-testing.ts +0 -17
- package/src/version.ts +0 -10
- package/src/widgets/__stories__/categorizer-editor.stories.tsx +0 -24
- package/src/widgets/__stories__/definition-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/dropdown-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/explanation-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/expression-editor.stories.tsx +0 -122
- package/src/widgets/__stories__/image-editor.stories.tsx +0 -72
- package/src/widgets/__stories__/input-number-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/interaction-editor.stories.tsx +0 -37
- package/src/widgets/__stories__/label-image-editor.stories.tsx +0 -99
- package/src/widgets/__stories__/matcher-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/number-line-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/numeric-input-editor.stories.tsx +0 -18
- package/src/widgets/__stories__/radio-editor.stories.tsx +0 -116
- package/src/widgets/__stories__/sorter-editor.stories.tsx +0 -18
- package/src/widgets/__tests__/categorizer-editor.test.tsx +0 -51
- package/src/widgets/__tests__/definition-editor.test.tsx +0 -41
- package/src/widgets/__tests__/dropdown-editor.test.tsx +0 -57
- package/src/widgets/__tests__/explanation-editor.test.tsx +0 -59
- package/src/widgets/__tests__/expression-editor.test.tsx +0 -403
- package/src/widgets/__tests__/input-number-editor.test.tsx +0 -128
- package/src/widgets/__tests__/matcher-editor.test.tsx +0 -47
- package/src/widgets/__tests__/number-line-editor.test.tsx +0 -144
- package/src/widgets/__tests__/numeric-input-editor.test.tsx +0 -177
- package/src/widgets/__tests__/radio-editor.test.tsx +0 -118
- package/src/widgets/__tests__/sorter-editor.test.tsx +0 -55
- package/src/widgets/categorizer-editor.tsx +0 -104
- package/src/widgets/cs-program-editor.tsx +0 -256
- package/src/widgets/definition-editor.tsx +0 -77
- package/src/widgets/deprecated-standin-editor.tsx +0 -29
- package/src/widgets/dropdown-editor.tsx +0 -211
- package/src/widgets/example-graphie-widget-editor.tsx +0 -70
- package/src/widgets/example-graphie-widget.tsx +0 -172
- package/src/widgets/example-widget-editor.tsx +0 -62
- package/src/widgets/example-widget.tsx +0 -154
- package/src/widgets/explanation-editor.tsx +0 -92
- package/src/widgets/expression-editor.tsx +0 -557
- package/src/widgets/graded-group-editor.tsx +0 -161
- package/src/widgets/graded-group-set-editor.tsx +0 -104
- package/src/widgets/grapher-editor.tsx +0 -155
- package/src/widgets/group-editor.tsx +0 -83
- package/src/widgets/iframe-editor.tsx +0 -202
- package/src/widgets/image-editor.tsx +0 -367
- package/src/widgets/input-number-editor.tsx +0 -288
- package/src/widgets/interaction-editor/constraint-editor.tsx +0 -134
- package/src/widgets/interaction-editor/function-editor.tsx +0 -108
- package/src/widgets/interaction-editor/index.ts +0 -3
- package/src/widgets/interaction-editor/interaction-editor.tsx +0 -749
- package/src/widgets/interaction-editor/label-editor.tsx +0 -87
- package/src/widgets/interaction-editor/line-editor.tsx +0 -125
- package/src/widgets/interaction-editor/mathquill-input.tsx +0 -81
- package/src/widgets/interaction-editor/movable-line-editor.tsx +0 -121
- package/src/widgets/interaction-editor/movable-point-editor.tsx +0 -89
- package/src/widgets/interaction-editor/parametric-editor.tsx +0 -117
- package/src/widgets/interaction-editor/point-editor.tsx +0 -72
- package/src/widgets/interaction-editor/rectangle-editor.tsx +0 -96
- package/src/widgets/interactive-graph-editor.tsx +0 -571
- package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +0 -82
- package/src/widgets/label-image/__stories__/behavior.stories.tsx +0 -31
- package/src/widgets/label-image/__stories__/marker.stories.tsx +0 -75
- package/src/widgets/label-image/__stories__/question-markers.stories.tsx +0 -107
- package/src/widgets/label-image/__stories__/select-image.stories.tsx +0 -59
- package/src/widgets/label-image/answer-choices.tsx +0 -231
- package/src/widgets/label-image/behavior.tsx +0 -122
- package/src/widgets/label-image/marker.tsx +0 -290
- package/src/widgets/label-image/question-markers.tsx +0 -176
- package/src/widgets/label-image/select-image.tsx +0 -81
- package/src/widgets/label-image-editor.tsx +0 -252
- package/src/widgets/lights-puzzle-editor.tsx +0 -278
- package/src/widgets/matcher-editor.tsx +0 -149
- package/src/widgets/matrix-editor.tsx +0 -156
- package/src/widgets/measurer-editor.tsx +0 -253
- package/src/widgets/molecule-editor.tsx +0 -64
- package/src/widgets/number-line-editor.tsx +0 -512
- package/src/widgets/numeric-input-editor.tsx +0 -536
- package/src/widgets/orderer-editor.tsx +0 -172
- package/src/widgets/passage-editor.tsx +0 -117
- package/src/widgets/passage-ref-editor.tsx +0 -80
- package/src/widgets/passage-ref-target-editor.tsx +0 -49
- package/src/widgets/plotter-editor.tsx +0 -542
- package/src/widgets/radio/editor.tsx +0 -398
- package/src/widgets/reaction-diagram-editor.tsx +0 -131
- package/src/widgets/sequence-editor.tsx +0 -205
- package/src/widgets/simple-markdown-tester-editor.tsx +0 -86
- package/src/widgets/simple-markdown-tester.tsx +0 -92
- package/src/widgets/simulator-editor.tsx +0 -146
- package/src/widgets/sorter-editor.tsx +0 -103
- package/src/widgets/table-editor.tsx +0 -164
- package/src/widgets/unit-editor.tsx +0 -299
- package/src/widgets/video-editor.tsx +0 -73
- package/tsconfig-build.json +0 -22
- 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;
|