@khanacademy/perseus-editor 0.0.1 → 0.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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/es/index.js +10 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +6 -1
- package/package.json +16 -4
- package/src/README.md +7 -0
- package/src/__stories__/editor.stories.js +33 -0
- package/src/__stories__/section-control-button.stories.jsx +27 -0
- package/src/__testdata__/input-number_testdata.js +28 -0
- package/src/__tests__/editor_test.jsx +123 -0
- package/src/__tests__/i18n_test.jsx +585 -0
- package/src/__tests__/traversal_test.jsx +378 -0
- package/src/all-editors.js +85 -0
- package/src/all-editors_test.js +17 -0
- package/src/all-widgets.js +10 -0
- package/src/article-editor.jsx +437 -0
- package/src/components/__stories__/blur-input.stories.jsx +24 -0
- package/src/components/blur-input.jsx +62 -0
- package/src/components/checkbox.jsx +194 -0
- package/src/components/device-framer.jsx +91 -0
- package/src/components/drag-target.jsx +88 -0
- package/src/components/dropdown-option.jsx +318 -0
- package/src/components/form-wrapped-text-field.jsx +248 -0
- package/src/components/graph-settings.jsx +631 -0
- package/src/components/hover-behavior.jsx +178 -0
- package/src/components/json-editor.jsx +118 -0
- package/src/components/link.jsx +173 -0
- package/src/components/simple-button.jsx +86 -0
- package/src/components/sortable.jsx +186 -0
- package/src/components/util.js +39 -0
- package/src/components/viewport-resizer.jsx +73 -0
- package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
- package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
- package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
- package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
- package/src/diffs/__tests__/split-diff_test.js +34 -0
- package/src/diffs/__tests__/string-array-diff_test.js +74 -0
- package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
- package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
- package/src/diffs/article-diff.jsx +70 -0
- package/src/diffs/item-diff.jsx +77 -0
- package/src/diffs/renderer-diff.jsx +143 -0
- package/src/diffs/split-diff.jsx +30 -0
- package/src/diffs/string-array-diff.jsx +74 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +193 -0
- package/src/diffs/widget-diff-performer.jsx +74 -0
- package/src/diffs/widget-diff.jsx +291 -0
- package/src/editor-page.jsx +351 -0
- package/src/editor.jsx +1364 -0
- package/src/hint-editor.jsx +447 -0
- package/src/i18n.jsx +139 -0
- package/src/iframe-content-renderer.jsx +201 -0
- package/src/index.js +27 -8
- package/src/item-editor.jsx +159 -0
- package/src/item-extras-editor.jsx +150 -0
- package/src/katex-error-view.jsx +87 -0
- package/src/multirenderer-editor.jsx +1073 -0
- package/src/section-control-button.jsx +53 -0
- package/src/stateful-article-editor.jsx +176 -0
- package/src/stateful-editor-page.jsx +90 -0
- package/src/styles/perseus-editor.less +1339 -0
- package/src/testing-widgets.js +12 -0
- package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
- package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
- package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
- package/src/widgets/categorizer-editor.jsx +103 -0
- package/src/widgets/cs-program-editor.jsx +258 -0
- package/src/widgets/definition-editor.jsx +76 -0
- package/src/widgets/dropdown-editor.jsx +201 -0
- package/src/widgets/example-graphie-widget-editor.jsx +70 -0
- package/src/widgets/example-graphie-widget.jsx +175 -0
- package/src/widgets/example-widget-editor.jsx +60 -0
- package/src/widgets/example-widget.jsx +153 -0
- package/src/widgets/explanation-editor.jsx +89 -0
- package/src/widgets/expression-editor.jsx +559 -0
- package/src/widgets/graded-group-editor.jsx +162 -0
- package/src/widgets/graded-group-set-editor.jsx +101 -0
- package/src/widgets/grapher-editor.jsx +154 -0
- package/src/widgets/group-editor.jsx +83 -0
- package/src/widgets/iframe-editor.jsx +200 -0
- package/src/widgets/image-editor.jsx +326 -0
- package/src/widgets/input-number-editor.jsx +269 -0
- package/src/widgets/interaction/constraint-editor.jsx +147 -0
- package/src/widgets/interaction-editor.jsx +1388 -0
- package/src/widgets/interactive-graph-editor.jsx +320 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
- package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
- package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
- package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
- package/src/widgets/label-image/answer-choices.jsx +231 -0
- package/src/widgets/label-image/behavior.jsx +92 -0
- package/src/widgets/label-image/marker.jsx +292 -0
- package/src/widgets/label-image/question-markers.jsx +181 -0
- package/src/widgets/label-image/select-image.jsx +86 -0
- package/src/widgets/label-image-editor.jsx +256 -0
- package/src/widgets/lights-puzzle-editor.jsx +267 -0
- package/src/widgets/matcher-editor.jsx +147 -0
- package/src/widgets/matrix-editor.jsx +155 -0
- package/src/widgets/measurer-editor.jsx +245 -0
- package/src/widgets/molecule-editor.jsx +63 -0
- package/src/widgets/number-line-editor.jsx +498 -0
- package/src/widgets/numeric-input-editor.jsx +554 -0
- package/src/widgets/orderer-editor.jsx +166 -0
- package/src/widgets/passage-editor.jsx +115 -0
- package/src/widgets/passage-ref-editor.jsx +77 -0
- package/src/widgets/passage-ref-target-editor.jsx +47 -0
- package/src/widgets/plotter-editor.jsx +537 -0
- package/src/widgets/radio/editor.jsx +399 -0
- package/src/widgets/reaction-diagram-editor.jsx +126 -0
- package/src/widgets/sequence-editor.jsx +205 -0
- package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
- package/src/widgets/simple-markdown-tester.jsx +91 -0
- package/src/widgets/simulator-editor.jsx +144 -0
- package/src/widgets/sorter-editor.jsx +100 -0
- package/src/widgets/table-editor.jsx +164 -0
- package/src/widgets/transformer-editor.jsx +1006 -0
- package/src/widgets/unit-editor.jsx +293 -0
- package/src/widgets/video-editor.jsx +74 -0
- package/src/__tests__/index.test.js +0 -15
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
2
|
+
// @flow
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An article editor. Articles are long-form pieces of content, composed of
|
|
6
|
+
* multiple (Renderer) sections concatenated together.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
components,
|
|
11
|
+
icons,
|
|
12
|
+
ApiOptions,
|
|
13
|
+
Changeable,
|
|
14
|
+
Dependencies,
|
|
15
|
+
Errors,
|
|
16
|
+
PerseusError,
|
|
17
|
+
} from "@khanacademy/perseus";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import _ from "underscore";
|
|
20
|
+
|
|
21
|
+
import DeviceFramer from "./components/device-framer.jsx";
|
|
22
|
+
import JsonEditor from "./components/json-editor.jsx";
|
|
23
|
+
import Editor from "./editor.jsx";
|
|
24
|
+
import IframeContentRenderer from "./iframe-content-renderer.jsx";
|
|
25
|
+
import SectionControlButton from "./section-control-button.jsx";
|
|
26
|
+
|
|
27
|
+
import type {APIOptions} from "@khanacademy/perseus";
|
|
28
|
+
|
|
29
|
+
const {HUD, InlineIcon} = components;
|
|
30
|
+
const {iconCircleArrowDown, iconCircleArrowUp, iconPlus, iconTrash} = icons;
|
|
31
|
+
|
|
32
|
+
type RendererProps = {|
|
|
33
|
+
content?: string,
|
|
34
|
+
widgets?: Object,
|
|
35
|
+
images?: Object,
|
|
36
|
+
|};
|
|
37
|
+
|
|
38
|
+
type JsonType = RendererProps | $ReadOnlyArray<RendererProps>;
|
|
39
|
+
type DefaultProps = {|
|
|
40
|
+
contentPaths?: $ReadOnlyArray<string>,
|
|
41
|
+
json: JsonType,
|
|
42
|
+
mode: "diff" | "edit" | "json" | "preview",
|
|
43
|
+
screen: "phone" | "tablet" | "desktop",
|
|
44
|
+
sectionImageUploadGenerator: (i: number) => React$Element<"span">,
|
|
45
|
+
useNewStyles: boolean,
|
|
46
|
+
|};
|
|
47
|
+
type PerseusArticleEditorProps = {|
|
|
48
|
+
...DefaultProps,
|
|
49
|
+
apiOptions?: APIOptions,
|
|
50
|
+
frameSource: string,
|
|
51
|
+
imageUploader?: (string, (string) => mixed) => mixed,
|
|
52
|
+
...Changeable.ChangeableProps,
|
|
53
|
+
|};
|
|
54
|
+
|
|
55
|
+
type State = {|
|
|
56
|
+
highlightLint: boolean,
|
|
57
|
+
|};
|
|
58
|
+
export default class ArticleEditor extends React.Component<
|
|
59
|
+
PerseusArticleEditorProps,
|
|
60
|
+
State,
|
|
61
|
+
> {
|
|
62
|
+
static defaultProps: DefaultProps = {
|
|
63
|
+
contentPaths: [],
|
|
64
|
+
// $FlowFixMe[incompatible-exact]
|
|
65
|
+
json: [{}],
|
|
66
|
+
mode: "edit",
|
|
67
|
+
screen: "desktop",
|
|
68
|
+
sectionImageUploadGenerator: () => <span />,
|
|
69
|
+
useNewStyles: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
state: State = {
|
|
73
|
+
highlightLint: true,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
componentDidMount() {
|
|
77
|
+
this._updatePreviewFrames();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
componentDidUpdate() {
|
|
81
|
+
this._updatePreviewFrames();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_updatePreviewFrames() {
|
|
85
|
+
if (this.props.mode === "preview") {
|
|
86
|
+
// eslint-disable-next-line react/no-string-refs
|
|
87
|
+
this.refs["frame-all"].sendNewData({
|
|
88
|
+
type: "article-all",
|
|
89
|
+
data: this._sections().map((section, i) => {
|
|
90
|
+
return this._apiOptionsForSection(section, i);
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
} else if (this.props.mode === "edit") {
|
|
94
|
+
this._sections().forEach((section, i) => {
|
|
95
|
+
// eslint-disable-next-line react/no-string-refs
|
|
96
|
+
this.refs["frame-" + i].sendNewData({
|
|
97
|
+
type: "article",
|
|
98
|
+
data: this._apiOptionsForSection(section, i),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_apiOptionsForSection(section: RendererProps, sectionIndex: number): any {
|
|
105
|
+
// eslint-disable-next-line react/no-string-refs
|
|
106
|
+
const editor = this.refs[`editor${sectionIndex}`];
|
|
107
|
+
return {
|
|
108
|
+
apiOptions: {
|
|
109
|
+
...ApiOptions.defaults,
|
|
110
|
+
...this.props.apiOptions,
|
|
111
|
+
|
|
112
|
+
// Alignment options are always available in article
|
|
113
|
+
// editors
|
|
114
|
+
showAlignmentOptions: true,
|
|
115
|
+
isArticle: true,
|
|
116
|
+
},
|
|
117
|
+
json: section,
|
|
118
|
+
useNewStyles: this.props.useNewStyles,
|
|
119
|
+
linterContext: {
|
|
120
|
+
contentType: "article",
|
|
121
|
+
highlightLint: this.state.highlightLint,
|
|
122
|
+
paths: this.props.contentPaths,
|
|
123
|
+
},
|
|
124
|
+
legacyPerseusLint: editor ? editor.getSaveWarnings() : [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_sections(): $ReadOnlyArray<RendererProps> {
|
|
129
|
+
return Array.isArray(this.props.json)
|
|
130
|
+
? this.props.json
|
|
131
|
+
: [this.props.json];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_renderEditor(): React$Element<"div"> {
|
|
135
|
+
const {imageUploader, sectionImageUploadGenerator} = this.props;
|
|
136
|
+
|
|
137
|
+
const apiOptions = {
|
|
138
|
+
...ApiOptions.defaults,
|
|
139
|
+
...this.props.apiOptions,
|
|
140
|
+
|
|
141
|
+
// Alignment options are always available in article editors
|
|
142
|
+
showAlignmentOptions: true,
|
|
143
|
+
isArticle: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const {KatexProvider} = Dependencies.getDependencies();
|
|
147
|
+
|
|
148
|
+
const sections = this._sections();
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="perseus-editor-table">
|
|
152
|
+
{sections.map((section, i) => {
|
|
153
|
+
return [
|
|
154
|
+
<div className="perseus-editor-row" key={i}>
|
|
155
|
+
<div className="perseus-editor-left-cell">
|
|
156
|
+
<div className="pod-title">
|
|
157
|
+
Section {i + 1}
|
|
158
|
+
<div
|
|
159
|
+
style={{
|
|
160
|
+
display: "inline-block",
|
|
161
|
+
float: "right",
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{sectionImageUploadGenerator(i)}
|
|
165
|
+
<SectionControlButton
|
|
166
|
+
icon={iconPlus}
|
|
167
|
+
onClick={() => {
|
|
168
|
+
this._handleAddSectionAfter(i);
|
|
169
|
+
}}
|
|
170
|
+
title="Add a new section after this one"
|
|
171
|
+
/>
|
|
172
|
+
{i + 1 < sections.length && (
|
|
173
|
+
<SectionControlButton
|
|
174
|
+
icon={iconCircleArrowDown}
|
|
175
|
+
onClick={() => {
|
|
176
|
+
this._handleMoveSectionLater(
|
|
177
|
+
i,
|
|
178
|
+
);
|
|
179
|
+
}}
|
|
180
|
+
title="Move this section down"
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
{i > 0 && (
|
|
184
|
+
<SectionControlButton
|
|
185
|
+
icon={iconCircleArrowUp}
|
|
186
|
+
onClick={() => {
|
|
187
|
+
this._handleMoveSectionEarlier(
|
|
188
|
+
i,
|
|
189
|
+
);
|
|
190
|
+
}}
|
|
191
|
+
title="Move this section up"
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
<SectionControlButton
|
|
195
|
+
icon={iconTrash}
|
|
196
|
+
onClick={() => {
|
|
197
|
+
const msg =
|
|
198
|
+
"Are you sure you " +
|
|
199
|
+
"want to delete section " +
|
|
200
|
+
(i + 1) +
|
|
201
|
+
"?";
|
|
202
|
+
/* eslint-disable no-alert */
|
|
203
|
+
if (confirm(msg)) {
|
|
204
|
+
this._handleRemoveSection(
|
|
205
|
+
i,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
/* eslint-enable no-alert */
|
|
209
|
+
}}
|
|
210
|
+
title="Delete this section"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<KatexProvider>
|
|
215
|
+
<Editor
|
|
216
|
+
{...section}
|
|
217
|
+
apiOptions={apiOptions}
|
|
218
|
+
imageUploader={imageUploader}
|
|
219
|
+
onChange={_.partial(
|
|
220
|
+
this._handleEditorChange,
|
|
221
|
+
i,
|
|
222
|
+
)}
|
|
223
|
+
placeholder="Type your section text here..."
|
|
224
|
+
ref={"editor" + i}
|
|
225
|
+
/>
|
|
226
|
+
</KatexProvider>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div className="editor-preview">
|
|
230
|
+
{this._renderIframePreview(i, true)}
|
|
231
|
+
</div>
|
|
232
|
+
</div>,
|
|
233
|
+
];
|
|
234
|
+
})}
|
|
235
|
+
{this._renderAddSection()}
|
|
236
|
+
{this._renderLinterHUD()}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
/* eslint-enable max-len */
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_renderAddSection(): React$Element<"div"> {
|
|
243
|
+
return (
|
|
244
|
+
<div className="perseus-editor-row">
|
|
245
|
+
<div className="perseus-editor-left-cell">
|
|
246
|
+
<a
|
|
247
|
+
href="#"
|
|
248
|
+
className="simple-button orange"
|
|
249
|
+
onClick={() => {
|
|
250
|
+
this._handleAddSectionAfter(
|
|
251
|
+
this._sections().length - 1,
|
|
252
|
+
);
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
<InlineIcon {...iconPlus} /> Add a section
|
|
256
|
+
</a>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_renderLinterHUD(): React$Element<any> {
|
|
263
|
+
return (
|
|
264
|
+
<HUD
|
|
265
|
+
message="Style warnings"
|
|
266
|
+
enabled={this.state.highlightLint}
|
|
267
|
+
onClick={() => {
|
|
268
|
+
this.setState({
|
|
269
|
+
highlightLint: !this.state.highlightLint,
|
|
270
|
+
});
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_renderIframePreview(
|
|
277
|
+
i: number | string,
|
|
278
|
+
nochrome: boolean,
|
|
279
|
+
): React$Element<any> {
|
|
280
|
+
const isMobile =
|
|
281
|
+
this.props.screen === "phone" || this.props.screen === "tablet";
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<DeviceFramer deviceType={this.props.screen} nochrome={nochrome}>
|
|
285
|
+
<IframeContentRenderer
|
|
286
|
+
ref={"frame-" + i}
|
|
287
|
+
key={this.props.screen}
|
|
288
|
+
content={this.props.frameSource}
|
|
289
|
+
datasetKey="mobile"
|
|
290
|
+
datasetValue={isMobile}
|
|
291
|
+
seamless={nochrome}
|
|
292
|
+
/>
|
|
293
|
+
</DeviceFramer>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_renderPreviewMode(): React$Element<"div"> {
|
|
298
|
+
return (
|
|
299
|
+
<div className="standalone-preview">
|
|
300
|
+
{this._renderIframePreview("all", false)}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_handleJsonChange: (newJson: JsonType) => void = (newJson) => {
|
|
306
|
+
this.props.onChange({json: newJson});
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
_handleEditorChange: (i: number, newProps: RendererProps) => void = (
|
|
310
|
+
i,
|
|
311
|
+
newProps,
|
|
312
|
+
) => {
|
|
313
|
+
const sections = _.clone(this._sections());
|
|
314
|
+
sections[i] = _.extend({}, sections[i], newProps);
|
|
315
|
+
this.props.onChange({json: sections});
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
_handleMoveSectionEarlier(i: number) {
|
|
319
|
+
if (i === 0) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const sections = _.clone(this._sections());
|
|
323
|
+
const section = sections[i];
|
|
324
|
+
sections.splice(i, 1);
|
|
325
|
+
sections.splice(i - 1, 0, section);
|
|
326
|
+
this.props.onChange({
|
|
327
|
+
json: sections,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_handleMoveSectionLater(i: number) {
|
|
332
|
+
const sections = _.clone(this._sections());
|
|
333
|
+
if (i + 1 === sections.length) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const section = sections[i];
|
|
337
|
+
sections.splice(i, 1);
|
|
338
|
+
sections.splice(i + 1, 0, section);
|
|
339
|
+
this.props.onChange({
|
|
340
|
+
json: sections,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_handleAddSectionAfter(i: number) {
|
|
345
|
+
// We do a full serialization here because we
|
|
346
|
+
// might be copying widgets:
|
|
347
|
+
const sections = _.clone(this.serialize());
|
|
348
|
+
// Here we do magic to allow you to copy-paste
|
|
349
|
+
// things from the previous section into the new
|
|
350
|
+
// section while preserving widgets.
|
|
351
|
+
// To enable this, we preserve the widgets
|
|
352
|
+
// object for the new section, but wipe out
|
|
353
|
+
// the content.
|
|
354
|
+
const newSection =
|
|
355
|
+
i >= 0
|
|
356
|
+
? {
|
|
357
|
+
widgets: sections[i].widgets,
|
|
358
|
+
}
|
|
359
|
+
: {};
|
|
360
|
+
sections.splice(i + 1, 0, newSection);
|
|
361
|
+
this.props.onChange({
|
|
362
|
+
json: sections,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_handleRemoveSection(i: number) {
|
|
367
|
+
const sections = _.clone(this._sections());
|
|
368
|
+
sections.splice(i, 1);
|
|
369
|
+
this.props.onChange({
|
|
370
|
+
json: sections,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
serialize(): JsonType {
|
|
375
|
+
if (this.props.mode === "edit") {
|
|
376
|
+
return this._sections().map((section, i) => {
|
|
377
|
+
// eslint-disable-next-line react/no-string-refs
|
|
378
|
+
return this.refs["editor" + i].serialize();
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (this.props.mode === "preview" || this.props.mode === "json") {
|
|
382
|
+
return this.props.json;
|
|
383
|
+
}
|
|
384
|
+
throw new PerseusError(
|
|
385
|
+
"Could not serialize; mode " + this.props.mode + " not found",
|
|
386
|
+
Errors.Internal,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Returns an array, with one element be section.
|
|
392
|
+
* Each element is an array of lint warnings present in that section.
|
|
393
|
+
*
|
|
394
|
+
* This function can currently only be called in edit mode.
|
|
395
|
+
*/
|
|
396
|
+
getSaveWarnings(): $ReadOnlyArray<RendererProps> {
|
|
397
|
+
if (this.props.mode !== "edit") {
|
|
398
|
+
// TODO(joshuan): We should be able to get save warnings in
|
|
399
|
+
// preview mode.
|
|
400
|
+
throw new PerseusError(
|
|
401
|
+
"Can only get save warnings in edit mode.",
|
|
402
|
+
Errors.NotAllowed,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return this._sections().map((section, i) => {
|
|
407
|
+
// eslint-disable-next-line react/no-string-refs
|
|
408
|
+
return this.refs["editor" + i].getSaveWarnings();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
render(): React.Node {
|
|
413
|
+
return (
|
|
414
|
+
<div className="framework-perseus perseus-article-editor">
|
|
415
|
+
{this.props.mode === "edit" && this._renderEditor()}
|
|
416
|
+
|
|
417
|
+
{this.props.mode === "preview" && this._renderPreviewMode()}
|
|
418
|
+
|
|
419
|
+
{this.props.mode === "json" && (
|
|
420
|
+
<div className="json-editor">
|
|
421
|
+
<div className="json-editor-warning">
|
|
422
|
+
<span>
|
|
423
|
+
Warning: Editing in this mode can lead to broken
|
|
424
|
+
articles!
|
|
425
|
+
</span>
|
|
426
|
+
</div>
|
|
427
|
+
<JsonEditor
|
|
428
|
+
multiLine={true}
|
|
429
|
+
onChange={this._handleJsonChange}
|
|
430
|
+
value={this.props.json}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import {action} from "@storybook/addon-actions";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import BlurInput from "../blur-input.jsx";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: "Perseus/Editor/Components/Blur Input",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Default = (): React.Node => {
|
|
13
|
+
const [value, setValue] = React.useState("");
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<BlurInput
|
|
17
|
+
value={value}
|
|
18
|
+
onChange={(newValue) => {
|
|
19
|
+
action("onChange")(newValue);
|
|
20
|
+
setValue(newValue);
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import type {CSSProperties} from "aphrodite";
|
|
5
|
+
|
|
6
|
+
type Props = {|
|
|
7
|
+
value: string,
|
|
8
|
+
onChange: (string) => void,
|
|
9
|
+
className?: string,
|
|
10
|
+
style?: CSSProperties,
|
|
11
|
+
|};
|
|
12
|
+
|
|
13
|
+
type State = {|
|
|
14
|
+
value: string,
|
|
15
|
+
|};
|
|
16
|
+
|
|
17
|
+
/* You know when you want to propagate input to a parent...
|
|
18
|
+
* but then that parent does something with the input...
|
|
19
|
+
* then changing the props of the input...
|
|
20
|
+
* on every keystroke...
|
|
21
|
+
* so if some input is invalid or incomplete...
|
|
22
|
+
* the input gets reset or otherwise effed...
|
|
23
|
+
*
|
|
24
|
+
* This is the solution.
|
|
25
|
+
*
|
|
26
|
+
* Enough melodrama. Its an input that only sends changes
|
|
27
|
+
* to its parent on blur.
|
|
28
|
+
*/
|
|
29
|
+
// eslint-disable-next-line react/no-unsafe
|
|
30
|
+
class BlurInput extends React.Component<Props, State> {
|
|
31
|
+
constructor(props: Props) {
|
|
32
|
+
super(props);
|
|
33
|
+
this.state = {value: this.props.value};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
37
|
+
this.setState({value: nextProps.value});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleChange: (e: $FlowFixMe) => void = (e) => {
|
|
41
|
+
this.setState({value: e.target.value});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
handleBlur: (e: $FlowFixMe) => void = (e) => {
|
|
45
|
+
this.props.onChange(e.target.value);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
render(): React.Element<"input"> {
|
|
49
|
+
return (
|
|
50
|
+
<input
|
|
51
|
+
className={this.props.className}
|
|
52
|
+
style={this.props.style}
|
|
53
|
+
type="text"
|
|
54
|
+
value={this.state.value}
|
|
55
|
+
onChange={this.handleChange}
|
|
56
|
+
onBlur={this.handleBlur}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default BlurInput;
|